新增信息维护年收入字段全链路支持

This commit is contained in:
wkc
2026-03-17 18:07:57 +08:00
parent 82cb751b8f
commit 8f9fc09338
33 changed files with 542 additions and 23 deletions

View File

@@ -5,6 +5,7 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
@@ -36,6 +37,9 @@ public class CcdiBaseStaff implements Serializable {
/** 电话 */
private String phone;
/** 年收入 */
private BigDecimal annualIncome;
/** 入职时间 */
private Date hireDate;

View File

@@ -8,6 +8,7 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
@@ -53,6 +54,9 @@ public class CcdiStaffFmyRelation implements Serializable {
/** 手机号码2 */
private String mobilePhone2;
/** 家庭成员年收入 */
private BigDecimal annualIncome;
/** 微信名称1 */
private String wechatNo1;

View File

@@ -8,6 +8,7 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
@@ -46,6 +47,9 @@ public class CcdiBaseStaffAddDTO implements Serializable {
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "电话格式不正确")
private String phone;
/** 年收入 */
private BigDecimal annualIncome;
/** 入职时间 */
private Date hireDate;

View File

@@ -8,6 +8,7 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
@@ -45,6 +46,9 @@ public class CcdiBaseStaffEditDTO implements Serializable {
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "电话格式不正确")
private String phone;
/** 年收入 */
private BigDecimal annualIncome;
/** 入职时间 */
private Date hireDate;

View File

@@ -9,6 +9,7 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
@@ -75,6 +76,10 @@ public class CcdiStaffFmyRelationAddDTO implements Serializable {
@Schema(description = "手机号码2")
private String mobilePhone2;
/** 家庭成员年收入 */
@Schema(description = "家庭成员年收入(元/年)")
private BigDecimal annualIncome;
/** 微信名称1 */
@Size(max = 50, message = "微信名称1长度不能超过50个字符")
@Schema(description = "微信名称1")

View File

@@ -10,6 +10,7 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
@@ -81,6 +82,10 @@ public class CcdiStaffFmyRelationEditDTO implements Serializable {
@Schema(description = "手机号码2")
private String mobilePhone2;
/** 家庭成员年收入 */
@Schema(description = "家庭成员年收入(元/年)")
private BigDecimal annualIncome;
/** 微信名称1 */
@Size(max = 50, message = "微信名称1长度不能超过50个字符")
@Schema(description = "微信名称1")

View File

@@ -8,6 +8,7 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
@@ -52,13 +53,18 @@ public class CcdiBaseStaffExcel implements Serializable {
@Required
private String phone;
/** 年收入 */
@ExcelProperty(value = "年收入(元/年)", index = 5)
@ColumnWidth(18)
private BigDecimal annualIncome;
/** 入职时间 */
@ExcelProperty(value = "入职时间", index = 5)
@ExcelProperty(value = "入职时间", index = 6)
@ColumnWidth(15)
private Date hireDate;
/** 状态 */
@ExcelProperty(value = "状态", index = 6)
@ExcelProperty(value = "状态", index = 7)
@ColumnWidth(10)
@DictDropdown(dictType = "ccdi_employee_status")
@Required

View File

@@ -9,6 +9,7 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
@@ -77,43 +78,48 @@ public class CcdiStaffFmyRelationExcel implements Serializable {
@ColumnWidth(15)
private String mobilePhone2;
/** 家庭成员年收入 */
@ExcelProperty(value = "家庭成员年收入(元/年)", index = 9)
@ColumnWidth(18)
private BigDecimal annualIncome;
/** 微信名称1 */
@ExcelProperty(value = "微信名称1", index = 9)
@ExcelProperty(value = "微信名称1", index = 10)
@ColumnWidth(15)
private String wechatNo1;
/** 微信名称2 */
@ExcelProperty(value = "微信名称2", index = 10)
@ExcelProperty(value = "微信名称2", index = 11)
@ColumnWidth(15)
private String wechatNo2;
/** 微信名称3 */
@ExcelProperty(value = "微信名称3", index = 11)
@ExcelProperty(value = "微信名称3", index = 12)
@ColumnWidth(15)
private String wechatNo3;
/** 详细联系地址 */
@ExcelProperty(value = "详细联系地址", index = 12)
@ExcelProperty(value = "详细联系地址", index = 13)
@ColumnWidth(30)
private String contactAddress;
/** 关系详细描述 */
@ExcelProperty(value = "关系详细描述", index = 13)
@ExcelProperty(value = "关系详细描述", index = 14)
@ColumnWidth(30)
private String relationDesc;
/** 生效日期 */
@ExcelProperty(value = "生效日期", index = 14)
@ExcelProperty(value = "生效日期", index = 15)
@ColumnWidth(15)
private Date effectiveDate;
/** 失效日期 */
@ExcelProperty(value = "失效日期", index = 15)
@ExcelProperty(value = "失效日期", index = 16)
@ColumnWidth(15)
private Date invalidDate;
/** 备注 */
@ExcelProperty(value = "备注", index = 16)
@ExcelProperty(value = "备注", index = 17)
@ColumnWidth(30)
private String remark;
}

View File

@@ -4,6 +4,7 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
@@ -37,6 +38,9 @@ public class CcdiBaseStaffVO implements Serializable {
/** 电话 */
private String phone;
/** 年收入 */
private BigDecimal annualIncome;
/** 入职时间 */
private Date hireDate;

View File

@@ -6,6 +6,7 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
@@ -71,6 +72,10 @@ public class CcdiStaffFmyRelationVO implements Serializable {
@Schema(description = "手机号码2")
private String mobilePhone2;
/** 家庭成员年收入 */
@Schema(description = "家庭成员年收入(元/年)")
private BigDecimal annualIncome;
/** 微信名称1 */
@Schema(description = "微信名称1")
private String wechatNo1;

View File

@@ -3,6 +3,8 @@ package com.ruoyi.info.collection.domain.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
/**
* 导入失败记录VO
*
@@ -27,6 +29,9 @@ public class ImportFailureVO {
@Schema(description = "电话")
private String phone;
@Schema(description = "年收入")
private BigDecimal annualIncome;
@Schema(description = "状态")
private String status;

View File

@@ -3,6 +3,8 @@ package com.ruoyi.info.collection.domain.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
/**
* 员工亲属关系信息导入失败记录VO
*
@@ -41,6 +43,10 @@ public class StaffFmyRelationImportFailureVO {
@Schema(description = "手机号码1")
private String mobilePhone1;
/** 家庭成员年收入 */
@Schema(description = "家庭成员年收入")
private BigDecimal annualIncome;
/** 状态 */
@Schema(description = "状态")
private Integer status;

View File

@@ -22,6 +22,7 @@ import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@@ -356,5 +357,19 @@ public class CcdiBaseStaffImportServiceImpl implements ICcdiBaseStaffImportServi
if (!"0".equals(addDTO.getStatus()) && !"1".equals(addDTO.getStatus())) {
throw new RuntimeException("状态只能填写'在职'或'离职'");
}
validateAnnualIncome(addDTO.getAnnualIncome(), "年收入");
}
private void validateAnnualIncome(BigDecimal annualIncome, String fieldLabel) {
if (annualIncome == null) {
return;
}
if (annualIncome.compareTo(BigDecimal.ZERO) < 0) {
throw new RuntimeException(fieldLabel + "不能为负数");
}
if (annualIncome.scale() > 2) {
throw new RuntimeException(fieldLabel + "最多保留2位小数");
}
}
}

View File

@@ -22,6 +22,7 @@ import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.*;
/**
@@ -129,6 +130,7 @@ public class CcdiBaseStaffServiceImpl implements ICcdiBaseStaffService {
@Override
@Transactional
public int insertBaseStaff(CcdiBaseStaffAddDTO addDTO) {
validateAnnualIncome(addDTO.getAnnualIncome(), "年收入");
// 检查员工ID唯一性
if (baseStaffMapper.selectById(addDTO.getStaffId()) != null) {
throw new RuntimeException("该员工ID已存在");
@@ -158,6 +160,7 @@ public class CcdiBaseStaffServiceImpl implements ICcdiBaseStaffService {
@Override
@Transactional
public int updateBaseStaff(CcdiBaseStaffEditDTO editDTO) {
validateAnnualIncome(editDTO.getAnnualIncome(), "年收入");
CcdiBaseStaff existing = baseStaffMapper.selectById(editDTO.getStaffId());
if (existing == null) {
throw new RuntimeException("员工不存在");
@@ -276,4 +279,16 @@ public class CcdiBaseStaffServiceImpl implements ICcdiBaseStaffService {
return vo;
}
private void validateAnnualIncome(BigDecimal annualIncome, String fieldLabel) {
if (annualIncome == null) {
return;
}
if (annualIncome.compareTo(BigDecimal.ZERO) < 0) {
throw new RuntimeException(fieldLabel + "不能为负数");
}
if (annualIncome.scale() > 2) {
throw new RuntimeException(fieldLabel + "最多保留2位小数");
}
}
}

View File

@@ -25,6 +25,7 @@ import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@@ -338,5 +339,19 @@ public class CcdiStaffFmyRelationImportServiceImpl implements ICcdiStaffFmyRelat
throw new RuntimeException("性别只能是:男、女、其他 或 M、F、O");
}
}
validateAnnualIncome(addDTO.getAnnualIncome(), "家庭成员年收入");
}
private void validateAnnualIncome(BigDecimal annualIncome, String fieldLabel) {
if (annualIncome == null) {
return;
}
if (annualIncome.compareTo(BigDecimal.ZERO) < 0) {
throw new RuntimeException(fieldLabel + "不能为负数");
}
if (annualIncome.scale() > 2) {
throw new RuntimeException(fieldLabel + "最多保留2位小数");
}
}
}

View File

@@ -21,6 +21,7 @@ import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -121,6 +122,7 @@ public class CcdiStaffFmyRelationServiceImpl implements ICcdiStaffFmyRelationSer
@Override
@Transactional
public int insertRelation(CcdiStaffFmyRelationAddDTO addDTO) {
validateAnnualIncome(addDTO.getAnnualIncome(), "家庭成员年收入");
CcdiStaffFmyRelation relation = new CcdiStaffFmyRelation();
BeanUtils.copyProperties(addDTO, relation);
@@ -146,6 +148,7 @@ public class CcdiStaffFmyRelationServiceImpl implements ICcdiStaffFmyRelationSer
@Override
@Transactional
public int updateRelation(CcdiStaffFmyRelationEditDTO editDTO) {
validateAnnualIncome(editDTO.getAnnualIncome(), "家庭成员年收入");
CcdiStaffFmyRelation existing = relationMapper.selectById(editDTO.getId());
if (existing == null) {
throw new RuntimeException("员工亲属关系不存在");
@@ -228,4 +231,16 @@ public class CcdiStaffFmyRelationServiceImpl implements ICcdiStaffFmyRelationSer
BeanUtils.copyProperties(assetInfo, assetInfoVO);
return assetInfoVO;
}
private void validateAnnualIncome(BigDecimal annualIncome, String fieldLabel) {
if (annualIncome == null) {
return;
}
if (annualIncome.compareTo(BigDecimal.ZERO) < 0) {
throw new RuntimeException(fieldLabel + "不能为负数");
}
if (annualIncome.scale() > 2) {
throw new RuntimeException(fieldLabel + "最多保留2位小数");
}
}
}

View File

@@ -12,6 +12,7 @@
<result property="deptName" column="dept_name"/>
<result property="idCard" column="id_card"/>
<result property="phone" column="phone"/>
<result property="annualIncome" column="annual_income"/>
<result property="hireDate" column="hire_date"/>
<result property="status" column="status"/>
<result property="createTime" column="create_time"/>
@@ -19,7 +20,7 @@
<select id="selectBaseStaffPageWithDept" resultMap="CcdiBaseStaffVOResult">
SELECT
e.staff_id, e.name, e.dept_id, e.id_card, e.phone, e.hire_date, e.status, e.create_time,
e.staff_id, e.name, e.dept_id, e.id_card, e.phone, e.annual_income, e.hire_date, e.status, e.create_time,
d.dept_name
FROM ccdi_base_staff e
LEFT JOIN sys_dept d ON e.dept_id = d.dept_id
@@ -46,18 +47,19 @@
<!-- 批量插入或更新员工信息只更新非null字段 -->
<insert id="insertOrUpdateBatch" parameterType="java.util.List">
INSERT INTO ccdi_base_staff
(staff_id, name, dept_id, id_card, phone, hire_date, status,
(staff_id, name, dept_id, id_card, phone, annual_income, hire_date, status,
create_time, create_by, update_by, update_time)
VALUES
<foreach collection="list" item="item" separator=",">
(#{item.staffId}, #{item.name}, #{item.deptId}, #{item.idCard},
#{item.phone}, #{item.hireDate}, #{item.status}, NOW(),
#{item.phone}, #{item.annualIncome}, #{item.hireDate}, #{item.status}, NOW(),
#{item.createBy}, #{item.updateBy}, NOW())
</foreach>
ON DUPLICATE KEY UPDATE
name = COALESCE(VALUES(name), name),
dept_id = COALESCE(VALUES(dept_id), dept_id),
phone = COALESCE(VALUES(phone), phone),
annual_income = COALESCE(VALUES(annual_income), annual_income),
hire_date = COALESCE(VALUES(hire_date), hire_date),
status = COALESCE(VALUES(status), status),
update_by = COALESCE(VALUES(update_by), update_by),
@@ -67,12 +69,12 @@
<!-- 批量插入员工信息 -->
<insert id="insertBatch" parameterType="java.util.List">
INSERT INTO ccdi_base_staff
(staff_id, name, dept_id, id_card, phone, hire_date, status,
(staff_id, name, dept_id, id_card, phone, annual_income, hire_date, status,
create_time, create_by, update_by, update_time)
VALUES
<foreach collection="list" item="item" separator=",">
(#{item.staffId}, #{item.name}, #{item.deptId}, #{item.idCard},
#{item.phone}, #{item.hireDate}, #{item.status}, NOW(),
#{item.phone}, #{item.annualIncome}, #{item.hireDate}, #{item.status}, NOW(),
#{item.createBy}, #{item.updateBy}, NOW())
</foreach>
</insert>

View File

@@ -17,6 +17,7 @@
<result property="relationCertNo" column="relation_cert_no"/>
<result property="mobilePhone1" column="mobile_phone1"/>
<result property="mobilePhone2" column="mobile_phone2"/>
<result property="annualIncome" column="annual_income"/>
<result property="wechatNo1" column="wechat_no1"/>
<result property="wechatNo2" column="wechat_no2"/>
<result property="wechatNo3" column="wechat_no3"/>
@@ -40,7 +41,7 @@
SELECT
r.id, r.person_id, s.name as person_name, r.relation_type, r.relation_name,
r.gender, r.birth_date, r.relation_cert_type, r.relation_cert_no,
r.mobile_phone1, r.mobile_phone2, r.wechat_no1, r.wechat_no2, r.wechat_no3,
r.mobile_phone1, r.mobile_phone2, r.annual_income, r.wechat_no1, r.wechat_no2, r.wechat_no3,
r.contact_address, r.relation_desc, r.effective_date, r.invalid_date,
r.status, r.remark, r.data_source, r.is_emp_family, r.is_cust_family,
r.created_by, r.create_time, r.updated_by, r.update_time
@@ -80,7 +81,7 @@
SELECT
r.id, r.person_id, s.name as person_name, r.relation_type, r.relation_name,
r.gender, r.birth_date, r.relation_cert_type, r.relation_cert_no,
r.mobile_phone1, r.mobile_phone2, r.wechat_no1, r.wechat_no2, r.wechat_no3,
r.mobile_phone1, r.mobile_phone2, r.annual_income, r.wechat_no1, r.wechat_no2, r.wechat_no3,
r.contact_address, r.relation_desc, r.effective_date, r.invalid_date,
r.status, r.remark, r.data_source, r.is_emp_family, r.is_cust_family,
r.created_by, r.create_time, r.updated_by, r.update_time
@@ -94,7 +95,7 @@
SELECT
r.id, r.person_id, s.name as person_name, r.relation_type, r.relation_name,
r.gender, r.birth_date, r.relation_cert_type, r.relation_cert_no,
r.mobile_phone1, r.mobile_phone2, r.wechat_no1, r.wechat_no2, r.wechat_no3,
r.mobile_phone1, r.mobile_phone2, r.annual_income, r.wechat_no1, r.wechat_no2, r.wechat_no3,
r.contact_address, r.relation_desc, r.effective_date, r.invalid_date,
r.status, r.remark, r.data_source, r.is_emp_family, r.is_cust_family,
r.created_by, r.create_time, r.updated_by, r.update_time
@@ -133,14 +134,14 @@
<insert id="insertBatch">
INSERT INTO ccdi_staff_fmy_relation
(person_id, relation_type, relation_name, gender, birth_date, relation_cert_type, relation_cert_no,
mobile_phone1, mobile_phone2, wechat_no1, wechat_no2, wechat_no3, contact_address, relation_desc,
mobile_phone1, mobile_phone2, annual_income, wechat_no1, wechat_no2, wechat_no3, contact_address, relation_desc,
effective_date, invalid_date, status, remark, data_source, is_emp_family, is_cust_family,
created_by, create_time, updated_by, update_time)
VALUES
<foreach collection="list" item="item" separator=",">
(#{item.personId}, #{item.relationType}, #{item.relationName}, #{item.gender},
#{item.birthDate}, #{item.relationCertType}, #{item.relationCertNo}, #{item.mobilePhone1},
#{item.mobilePhone2}, #{item.wechatNo1}, #{item.wechatNo2}, #{item.wechatNo3}, #{item.contactAddress},
#{item.mobilePhone2}, #{item.annualIncome}, #{item.wechatNo1}, #{item.wechatNo2}, #{item.wechatNo3}, #{item.contactAddress},
#{item.relationDesc}, #{item.effectiveDate}, #{item.invalidDate}, #{item.status}, #{item.remark},
#{item.dataSource}, #{item.isEmpFamily}, #{item.isCustFamily}, #{item.createdBy}, NOW(), #{item.updatedBy}, NOW())
</foreach>

View File

@@ -0,0 +1,22 @@
package com.ruoyi.info.collection.mapper;
import org.junit.jupiter.api.Test;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CcdiBaseStaffMapperTest {
@Test
void mapperXml_shouldContainAnnualIncomeColumnsInCustomSql() throws Exception {
try (InputStream inputStream = getClass().getClassLoader()
.getResourceAsStream("mapper/info/collection/CcdiBaseStaffMapper.xml")) {
String xml = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
assertTrue(xml.contains("annual_income"), xml);
assertTrue(xml.contains("#{item.annualIncome}"), xml);
}
}
}

View File

@@ -41,6 +41,16 @@ class CcdiStaffFmyRelationMapperTest {
assertTrue(sql.contains("WHERE 1 = 1 AND r.is_emp_family = 1 AND r.person_id = ?"), sql);
assertFalse(sql.contains("1AND"), sql);
assertFalse(countSql.contains("1AND"), countSql);
assertTrue(sql.contains("r.annual_income"), sql);
}
@Test
void mapperXml_shouldContainAnnualIncomeInBatchInsert() throws Exception {
try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(RESOURCE)) {
String xml = new String(inputStream.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8);
assertTrue(xml.contains("annual_income"), xml);
assertTrue(xml.contains("#{item.annualIncome}"), xml);
}
}
private MappedStatement loadMappedStatement(String statementId) throws Exception {

View File

@@ -0,0 +1,57 @@
package com.ruoyi.info.collection.service;
import com.ruoyi.info.collection.domain.dto.CcdiBaseStaffAddDTO;
import com.ruoyi.info.collection.service.impl.CcdiBaseStaffImportServiceImpl;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class CcdiBaseStaffImportServiceImplTest {
private final CcdiBaseStaffImportServiceImpl service = new CcdiBaseStaffImportServiceImpl();
@Test
void validateStaffData_shouldAllowEmptyAnnualIncome() {
assertDoesNotThrow(() -> service.validateStaffData(buildDto(null), false, Collections.emptySet(), Collections.emptySet()));
}
@Test
void validateStaffData_shouldAllowZeroAndTwoDecimalAnnualIncome() {
assertDoesNotThrow(() -> service.validateStaffData(buildDto(new BigDecimal("0.00")), false, Collections.emptySet(), Collections.emptySet()));
assertDoesNotThrow(() -> service.validateStaffData(buildDto(new BigDecimal("12345.67")), false, Collections.emptySet(), Collections.emptySet()));
}
@Test
void validateStaffData_shouldRejectNegativeAnnualIncome() {
RuntimeException exception = assertThrows(RuntimeException.class,
() -> service.validateStaffData(buildDto(new BigDecimal("-1.00")), false, Set.of(), Set.of()));
assertEquals("年收入不能为负数", exception.getMessage());
}
@Test
void validateStaffData_shouldRejectAnnualIncomeWithMoreThanTwoDecimals() {
RuntimeException exception = assertThrows(RuntimeException.class,
() -> service.validateStaffData(buildDto(new BigDecimal("12.345")), false, Set.of(), Set.of()));
assertEquals("年收入最多保留2位小数", exception.getMessage());
}
private CcdiBaseStaffAddDTO buildDto(BigDecimal annualIncome) {
CcdiBaseStaffAddDTO dto = new CcdiBaseStaffAddDTO();
dto.setName("张三");
dto.setStaffId(1001L);
dto.setDeptId(10L);
dto.setIdCard("320101199001010014");
dto.setPhone("13812345678");
dto.setStatus("0");
dto.setAnnualIncome(annualIncome);
return dto;
}
}

View File

@@ -55,6 +55,7 @@ class CcdiBaseStaffServiceImplTest {
addDTO.setIdCard("320101199001010011");
addDTO.setPhone("13812345678");
addDTO.setStatus("0");
addDTO.setAnnualIncome(new BigDecimal("12345.67"));
addDTO.setAssetInfoList(List.of(
buildAssetDto("房产"),
buildAssetDto("车辆")
@@ -67,6 +68,9 @@ class CcdiBaseStaffServiceImplTest {
int result = service.insertBaseStaff(addDTO);
assertEquals(1, result);
ArgumentCaptor<CcdiBaseStaff> staffCaptor = ArgumentCaptor.forClass(CcdiBaseStaff.class);
verify(baseStaffMapper).insert(staffCaptor.capture());
assertEquals(new BigDecimal("12345.67"), staffCaptor.getValue().getAnnualIncome());
ArgumentCaptor<List<CcdiAssetInfoDTO>> captor = ArgumentCaptor.forClass(List.class);
verify(assetInfoService).replaceByFamilyId(eq("320101199001010011"), captor.capture());
List<CcdiAssetInfoDTO> savedAssets = captor.getValue();
@@ -88,6 +92,7 @@ class CcdiBaseStaffServiceImplTest {
editDTO.setIdCard("320101199001010011");
editDTO.setPhone("13812345678");
editDTO.setStatus("0");
editDTO.setAnnualIncome(new BigDecimal("45678.90"));
editDTO.setAssetInfoList(List.of(buildAssetDto("车辆")));
when(baseStaffMapper.selectById(1001L)).thenReturn(existing);
@@ -97,6 +102,9 @@ class CcdiBaseStaffServiceImplTest {
int result = service.updateBaseStaff(editDTO);
assertEquals(1, result);
ArgumentCaptor<CcdiBaseStaff> staffCaptor = ArgumentCaptor.forClass(CcdiBaseStaff.class);
verify(baseStaffMapper).updateById(staffCaptor.capture());
assertEquals(new BigDecimal("45678.90"), staffCaptor.getValue().getAnnualIncome());
verify(assetInfoService, never()).deleteByFamilyId("320101199001010011");
verify(assetInfoService).replaceByFamilyId("320101199001010011", editDTO.getAssetInfoList());
}
@@ -133,6 +141,7 @@ class CcdiBaseStaffServiceImplTest {
staff.setName("张三");
staff.setIdCard("320101199001010011");
staff.setStatus("0");
staff.setAnnualIncome(new BigDecimal("88888.88"));
CcdiAssetInfo assetInfo = new CcdiAssetInfo();
assetInfo.setFamilyId("320101199001010011");
@@ -149,6 +158,7 @@ class CcdiBaseStaffServiceImplTest {
CcdiBaseStaffVO result = service.selectBaseStaffById(1001L);
assertNotNull(result.getAssetInfoList());
assertEquals(new BigDecimal("88888.88"), result.getAnnualIncome());
assertEquals(1, result.getAssetInfoList().size());
assertEquals("320101199201010022", result.getAssetInfoList().get(0).getPersonId());
assertEquals("车辆", result.getAssetInfoList().get(0).getAssetMainType());

View File

@@ -0,0 +1,59 @@
package com.ruoyi.info.collection.service;
import com.ruoyi.info.collection.domain.dto.CcdiStaffFmyRelationAddDTO;
import com.ruoyi.info.collection.service.impl.CcdiStaffFmyRelationImportServiceImpl;
import org.junit.jupiter.api.Test;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class CcdiStaffFmyRelationImportServiceImplTest {
private final CcdiStaffFmyRelationImportServiceImpl service = new CcdiStaffFmyRelationImportServiceImpl();
@Test
void validateRelationData_shouldRejectNegativeAnnualIncome() throws Exception {
RuntimeException exception = assertThrows(RuntimeException.class,
() -> invokeValidateRelationData(buildDto(new BigDecimal("-1.00"))));
assertEquals("家庭成员年收入不能为负数", exception.getMessage());
}
@Test
void validateRelationData_shouldRejectAnnualIncomeWithMoreThanTwoDecimals() throws Exception {
RuntimeException exception = assertThrows(RuntimeException.class,
() -> invokeValidateRelationData(buildDto(new BigDecimal("12.345"))));
assertEquals("家庭成员年收入最多保留2位小数", exception.getMessage());
}
private void invokeValidateRelationData(CcdiStaffFmyRelationAddDTO dto) throws Exception {
Method method = CcdiStaffFmyRelationImportServiceImpl.class
.getDeclaredMethod("validateRelationData", CcdiStaffFmyRelationAddDTO.class);
method.setAccessible(true);
try {
method.invoke(service, dto);
} catch (InvocationTargetException ex) {
Throwable targetException = ex.getTargetException();
if (targetException instanceof RuntimeException runtimeException) {
throw runtimeException;
}
throw ex;
}
}
private CcdiStaffFmyRelationAddDTO buildDto(BigDecimal annualIncome) {
CcdiStaffFmyRelationAddDTO dto = new CcdiStaffFmyRelationAddDTO();
dto.setPersonId("320101199001010014");
dto.setRelationType("配偶");
dto.setRelationName("李四");
dto.setRelationCertType("护照");
dto.setRelationCertNo("A123456789");
dto.setAnnualIncome(annualIncome);
return dto;
}
}

View File

@@ -77,6 +77,7 @@ class CcdiStaffFmyRelationServiceImplTest {
addDTO.setRelationName("李四");
addDTO.setRelationCertType("护照");
addDTO.setRelationCertNo("A123456789");
addDTO.setAnnualIncome(new BigDecimal("23456.78"));
addDTO.setAssetInfoList(List.of(buildAssetDto("房产")));
when(relationMapper.insert(any(CcdiStaffFmyRelation.class))).thenReturn(1);
@@ -89,6 +90,7 @@ class CcdiStaffFmyRelationServiceImplTest {
assertEquals("MANUAL", relationCaptor.getValue().getDataSource());
assertEquals(Boolean.TRUE, relationCaptor.getValue().getIsEmpFamily());
assertEquals(Boolean.FALSE, relationCaptor.getValue().getIsCustFamily());
assertEquals(new BigDecimal("23456.78"), relationCaptor.getValue().getAnnualIncome());
verify(assetInfoService).replaceByFamilyIdAndPersonId("320101199001010011", "A123456789", addDTO.getAssetInfoList());
}
@@ -154,6 +156,7 @@ class CcdiStaffFmyRelationServiceImplTest {
editDTO.setRelationName("李四");
editDTO.setRelationCertType("护照");
editDTO.setRelationCertNo("A123456789");
editDTO.setAnnualIncome(new BigDecimal("76543.21"));
editDTO.setAssetInfoList(List.of(buildAssetDto("车辆")));
when(relationMapper.selectById(10L)).thenReturn(existing);
@@ -162,6 +165,9 @@ class CcdiStaffFmyRelationServiceImplTest {
int result = service.updateRelation(editDTO);
assertEquals(1, result);
ArgumentCaptor<CcdiStaffFmyRelation> relationCaptor = ArgumentCaptor.forClass(CcdiStaffFmyRelation.class);
verify(relationMapper).updateById(relationCaptor.capture());
assertEquals(new BigDecimal("76543.21"), relationCaptor.getValue().getAnnualIncome());
verify(assetInfoService).replaceByFamilyIdAndPersonId("320101199001010011", "A123456789", editDTO.getAssetInfoList());
}

View File

@@ -0,0 +1,37 @@
# 信息维护年收入字段后端实施计划
## 目标
- 员工信息维护新增 `annual_income`
- 员工亲属关系维护新增 `annual_income`
- 覆盖数据库、实体、DTO/VO、Mapper、导入导出、失败记录、服务层校验
## 实施内容
1. 数据库变更
-`ccdi_base_staff` 新增 `annual_income DECIMAL(15,2) NULL`
-`ccdi_staff_fmy_relation` 新增 `annual_income DECIMAL(15,2) NULL`
- 增量脚本落库到 `sql/migration/2026-03-17-add-annual-income-to-info-maintenance.sql`
2. 员工信息链路
- 扩展 `CcdiBaseStaff``CcdiBaseStaffAddDTO``CcdiBaseStaffEditDTO``CcdiBaseStaffVO`
- 扩展 `CcdiBaseStaffExcel``ImportFailureVO`
- 更新 `CcdiBaseStaffMapper.xml` 的分页查询、批量插入、批量更新 SQL
-`CcdiBaseStaffServiceImpl``CcdiBaseStaffImportServiceImpl` 增加非负且最多两位小数校验
3. 员工亲属关系链路
- 扩展 `CcdiStaffFmyRelation``CcdiStaffFmyRelationAddDTO``CcdiStaffFmyRelationEditDTO``CcdiStaffFmyRelationVO`
- 扩展 `CcdiStaffFmyRelationExcel``StaffFmyRelationImportFailureVO`
- 更新 `CcdiStaffFmyRelationMapper.xml` 的列表、详情、导出、批量插入 SQL
-`CcdiStaffFmyRelationServiceImpl``CcdiStaffFmyRelationImportServiceImpl` 增加非负且最多两位小数校验
## 测试
- `CcdiBaseStaffServiceImplTest`
- `CcdiStaffFmyRelationServiceImplTest`
- `CcdiBaseStaffImportServiceImplTest`
- `CcdiStaffFmyRelationImportServiceImplTest`
- `CcdiBaseStaffMapperTest`
- `CcdiStaffFmyRelationMapperTest`
## 验收标准
- 列表、详情、导出和导入都能透传 `annualIncome`
- 服务层可拦截负数与超过两位小数
- 失败记录可返回原始年收入值

View File

@@ -0,0 +1,38 @@
# 信息维护年收入字段前端实施计划
## 目标
- 在员工信息维护页面新增“年收入”
- 在员工亲属关系维护页面新增“家庭成员年收入”
- 保持现有页面结构和接口调用方式不变
## 实施内容
1. 员工信息维护页
- 列表新增“年收入”列
- 新增/编辑弹窗新增“年收入”输入框
- 详情弹窗展示“年收入”
- 导入失败记录弹窗新增“年收入”列
- 提交前做空值兼容与金额格式校验
2. 员工亲属关系维护页
- 列表新增“家庭成员年收入”列
- 新增/编辑弹窗新增“家庭成员年收入”输入框
- 详情弹窗展示“家庭成员年收入”
- 导入失败记录弹窗新增“家庭成员年收入”列
- 提交前做空值兼容与金额格式校验
3. 前端校验规则
- 允许空值
- 非空时仅允许非负金额
- 最多保留两位小数
- 不做单位换算,直接按“元/年”展示和提交
## 测试
- `ruoyi-ui/tests/unit/employee-asset-submit-flow.test.js`
- `ruoyi-ui/tests/unit/employee-asset-maintenance-layout.test.js`
- `ruoyi-ui/tests/unit/staff-family-asset-submit-flow.test.js`
- `ruoyi-ui/tests/unit/staff-family-asset-maintenance-layout.test.js`
## 验收标准
- 两个页面都能新增、编辑、查看年收入字段
- 历史空值数据页面正常展示
- 前端能阻止非法金额提交

View File

@@ -115,6 +115,7 @@
<el-table-column label="身份证号" align="center" prop="idCard" :show-overflow-tooltip="true"/>
<el-table-column label="所属部门" align="center" prop="deptName" :show-overflow-tooltip="true"/>
<el-table-column label="电话" align="center" prop="phone" width="120"/>
<el-table-column label="年收入" align="center" prop="annualIncome" width="140"/>
<el-table-column label="状态" align="center" prop="status" width="100">
<template slot-scope="scope">
<el-tag v-if="scope.row.status === '0'" type="success">在职</el-tag>
@@ -204,11 +205,19 @@
<el-input v-model="form.phone" placeholder="请输入电话" maxlength="11" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="年收入" prop="annualIncome">
<el-input v-model="form.annualIncome" placeholder="请输入年收入(元/年)" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="入职时间" prop="hireDate">
<el-date-picker v-model="form.hireDate" type="date" placeholder="选择入职时间" value-format="yyyy-MM-dd" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12" />
</el-row>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
@@ -318,6 +327,9 @@
<el-descriptions-item label="所属部门">{{ employeeDetail.deptName || '-' }}</el-descriptions-item>
<el-descriptions-item label="身份证号">{{ employeeDetail.idCard || '-' }}</el-descriptions-item>
<el-descriptions-item label="电话">{{ employeeDetail.phone || '-' }}</el-descriptions-item>
<el-descriptions-item label="年收入">
{{ employeeDetail.annualIncome !== null && employeeDetail.annualIncome !== undefined && employeeDetail.annualIncome !== '' ? employeeDetail.annualIncome : '-' }}
</el-descriptions-item>
<el-descriptions-item label="入职时间">
{{ employeeDetail.hireDate ? parseTime(employeeDetail.hireDate, '{y}-{m}-{d}') : '-' }}
</el-descriptions-item>
@@ -453,6 +465,7 @@
<el-table-column label="柜员号" prop="staffId" align="center" />
<el-table-column label="身份证号" prop="idCard" align="center" />
<el-table-column label="电话" prop="phone" align="center" />
<el-table-column label="年收入" prop="annualIncome" align="center" width="140" />
<el-table-column label="失败原因" prop="errorMessage" align="center" min-width="200" :show-overflow-tooltip="true" />
</el-table>
@@ -606,6 +619,9 @@ export default {
{ required: true, message: "电话不能为空", trigger: "blur" },
{ pattern: phonePattern, message: "请输入正确的11位手机号", trigger: "blur" }
],
annualIncome: [
{ validator: (rule, value, callback) => this.validateAnnualIncomeRule(value, callback, "年收入"), trigger: "blur" }
],
status: [
{ required: true, message: "请选择状态", trigger: "change" }
]
@@ -938,6 +954,7 @@ export default {
deptId: null,
idCard: null,
phone: null,
annualIncome: null,
hireDate: null,
status: "0",
relatives: [],
@@ -945,6 +962,36 @@ export default {
};
this.resetForm("form");
},
normalizeAnnualIncome(value) {
if (value === null || value === undefined) {
return null;
}
const normalized = String(value).trim();
return normalized === "" ? null : normalized;
},
validateAnnualIncome(value, fieldLabel = "年收入") {
const normalized = this.normalizeAnnualIncome(value);
if (normalized === null) {
return true;
}
if (!/^\d+(\.\d{1,2})?$/.test(normalized)) {
this.$modal.msgError(`${fieldLabel}格式不正确需为非负金额且最多保留2位小数`);
return false;
}
return true;
},
validateAnnualIncomeRule(value, callback, fieldLabel = "年收入") {
const normalized = this.normalizeAnnualIncome(value);
if (normalized === null) {
callback();
return;
}
if (!/^\d+(\.\d{1,2})?$/.test(normalized)) {
callback(new Error(`${fieldLabel}格式不正确需为非负金额且最多保留2位小数`));
return;
}
callback();
},
normalizeAssetInfoList() {
const assetInfoList = Array.isArray(this.form.assetInfoList)
? this.form.assetInfoList
@@ -1114,6 +1161,7 @@ export default {
...response.data,
assetInfoList: response.data.assetInfoList || []
};
this.form.annualIncome = response.data.annualIncome;
this.open = true;
this.title = "编辑员工";
});
@@ -1123,18 +1171,27 @@ export default {
this.$refs["form"].validate(valid => {
if (valid) {
this.form.assetInfoList = this.normalizeAssetInfoList();
if (!this.validateAssetInfoList(this.form.assetInfoList)) {
const payload = {
...this.form,
assetInfoList: this.form.assetInfoList
};
payload.annualIncome = this.normalizeAnnualIncome(payload.annualIncome);
this.form.assetInfoList = payload.assetInfoList;
if (!this.validateAnnualIncome(payload.annualIncome, "年收入")) {
return;
}
if (!this.validateAssetInfoList(payload.assetInfoList)) {
return;
}
if (this.isAdd) {
addBaseStaff(this.form).then(response => {
addBaseStaff(payload).then(response => {
this.$modal.msgSuccess("新增成功");
this.open = false;
this.isAdd = false;
this.getList();
});
} else {
updateBaseStaff(this.form).then(response => {
updateBaseStaff(payload).then(response => {
this.$modal.msgSuccess("修改成功");
this.open = false;
this.getList();

View File

@@ -123,6 +123,7 @@
</template>
</el-table-column>
<el-table-column label="关系人姓名" align="center" prop="relationName" :show-overflow-tooltip="true"/>
<el-table-column label="家庭成员年收入" align="center" prop="annualIncome" width="160"/>
<el-table-column label="性别" align="center" prop="gender" width="80">
<template slot-scope="scope">
<dict-tag :options="dict.type.ccdi_indiv_gender" :value="scope.row.gender"/>
@@ -282,6 +283,13 @@
<el-input v-model="form.mobilePhone2" placeholder="请输入手机号码2" maxlength="20" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="家庭成员年收入" prop="annualIncome">
<el-input v-model="form.annualIncome" placeholder="请输入家庭成员年收入(元/年)" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="微信名称1" prop="wechatNo1">
<el-input v-model="form.wechatNo1" placeholder="请输入微信名称1" maxlength="50" />
@@ -466,6 +474,9 @@
<el-descriptions-item label="出生日期">{{ relationDetail.birthDate || '-' }}</el-descriptions-item>
<el-descriptions-item label="手机号码1">{{ relationDetail.mobilePhone1 || '-' }}</el-descriptions-item>
<el-descriptions-item label="手机号码2">{{ relationDetail.mobilePhone2 || '-' }}</el-descriptions-item>
<el-descriptions-item label="家庭成员年收入">
{{ relationDetail.annualIncome !== null && relationDetail.annualIncome !== undefined && relationDetail.annualIncome !== '' ? relationDetail.annualIncome : '-' }}
</el-descriptions-item>
<el-descriptions-item label="微信名称1">{{ relationDetail.wechatNo1 || '-' }}</el-descriptions-item>
<el-descriptions-item label="微信名称2">{{ relationDetail.wechatNo2 || '-' }}</el-descriptions-item>
<el-descriptions-item label="微信名称3">{{ relationDetail.wechatNo3 || '-' }}</el-descriptions-item>
@@ -616,6 +627,7 @@
<el-table-column label="员工身份证号" prop="personId" align="center" width="180"/>
<el-table-column label="关系类型" prop="relationType" align="center" width="100"/>
<el-table-column label="关系人姓名" prop="relationName" align="center" width="120"/>
<el-table-column label="家庭成员年收入" prop="annualIncome" align="center" width="160"/>
<el-table-column label="失败原因" prop="errorMessage" align="center" min-width="200" :show-overflow-tooltip="true" />
</el-table>
@@ -766,6 +778,9 @@ export default {
],
mobilePhone2: [
{ pattern: /^1[3-9]\d{9}$/, message: "请输入正确的手机号码", trigger: "blur" }
],
annualIncome: [
{ validator: (rule, value, callback) => this.validateAnnualIncomeRule(value, callback, "家庭成员年收入"), trigger: "blur" }
]
},
// 导入参数
@@ -963,6 +978,7 @@ export default {
relationCertNo: null,
mobilePhone1: null,
mobilePhone2: null,
annualIncome: null,
wechatNo1: null,
wechatNo2: null,
wechatNo3: null,
@@ -977,6 +993,36 @@ export default {
this.staffOptions = [];
this.resetForm("form");
},
normalizeAnnualIncome(value) {
if (value === null || value === undefined) {
return null;
}
const normalized = String(value).trim();
return normalized === "" ? null : normalized;
},
validateAnnualIncome(value, fieldLabel = "家庭成员年收入") {
const normalized = this.normalizeAnnualIncome(value);
if (normalized === null) {
return true;
}
if (!/^\d+(\.\d{1,2})?$/.test(normalized)) {
this.$modal.msgError(`${fieldLabel}格式不正确需为非负金额且最多保留2位小数`);
return false;
}
return true;
},
validateAnnualIncomeRule(value, callback, fieldLabel = "家庭成员年收入") {
const normalized = this.normalizeAnnualIncome(value);
if (normalized === null) {
callback();
return;
}
if (!/^\d+(\.\d{1,2})?$/.test(normalized)) {
callback(new Error(`${fieldLabel}格式不正确需为非负金额且最多保留2位小数`));
return;
}
callback();
},
createEmptyAssetRow() {
return {
assetMainType: "",
@@ -1098,6 +1144,7 @@ export default {
...response.data,
assetInfoList: response.data.assetInfoList || []
};
this.form.annualIncome = response.data.annualIncome;
// 加载员工信息以支持下拉显示
if (this.form.personId) {
this.searchStaff(this.form.personId);
@@ -1131,6 +1178,10 @@ export default {
...this.form,
assetInfoList: this.form.assetInfoList.map(item => ({ ...item }))
};
payload.annualIncome = this.normalizeAnnualIncome(payload.annualIncome);
if (!this.validateAnnualIncome(payload.annualIncome, "家庭成员年收入")) {
return;
}
payload.assetInfoList.forEach((asset, index) => {
delete payload.assetInfoList[index].familyId;
delete payload.assetInfoList[index].personId;

View File

@@ -9,6 +9,9 @@ const componentPath = path.resolve(
const source = fs.readFileSync(componentPath, "utf8");
[
"年收入",
"v-model=\"form.annualIncome\"",
"employeeDetail.annualIncome",
"<el-dialog :title=\"title\" :visible.sync=\"open\" width=\"80%\"",
"<el-dialog title=\"员工详情\" :visible.sync=\"detailOpen\" width=\"80%\"",
"资产信息",

View File

@@ -9,6 +9,13 @@ const componentPath = path.resolve(
const source = fs.readFileSync(componentPath, "utf8");
[
"annualIncome: null",
"label=\"年收入\"",
"v-model=\"form.annualIncome\"",
"this.form.annualIncome = response.data.annualIncome",
"payload.annualIncome = this.normalizeAnnualIncome(payload.annualIncome);",
"normalizeAnnualIncome(value)",
"validateAnnualIncome(value, fieldLabel = \"年收入\")",
"assetInfoList: []",
"normalizeAssetInfoList()",
"this.form.assetInfoList = [];",

View File

@@ -9,6 +9,9 @@ const componentPath = path.resolve(
const source = fs.readFileSync(componentPath, "utf8");
[
"家庭成员年收入",
"v-model=\"form.annualIncome\"",
"relationDetail.annualIncome",
"亲属资产信息",
"新增资产",
"handleAddAsset()",

View File

@@ -9,6 +9,13 @@ const componentPath = path.resolve(
const source = fs.readFileSync(componentPath, "utf8");
[
"annualIncome: null",
"label=\"家庭成员年收入\"",
"v-model=\"form.annualIncome\"",
"this.form.annualIncome = response.data.annualIncome",
"payload.annualIncome = this.normalizeAnnualIncome(payload.annualIncome);",
"normalizeAnnualIncome(value)",
"validateAnnualIncome(value, fieldLabel = \"家庭成员年收入\")",
"assetInfoList: []",
"normalizeAssetInfoList()",
"response.data.assetInfoList || []",

View File

@@ -0,0 +1,31 @@
SET @base_staff_annual_income_sql = IF(
EXISTS(
SELECT 1
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 'ccdi_base_staff'
AND column_name = 'annual_income'
),
'SELECT 1',
'ALTER TABLE `ccdi_base_staff` ADD COLUMN `annual_income` DECIMAL(15, 2) NULL COMMENT ''年收入(元/年)'' AFTER `phone`'
);
PREPARE base_staff_annual_income_stmt FROM @base_staff_annual_income_sql;
EXECUTE base_staff_annual_income_stmt;
DEALLOCATE PREPARE base_staff_annual_income_stmt;
SET @staff_fmy_relation_annual_income_sql = IF(
EXISTS(
SELECT 1
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 'ccdi_staff_fmy_relation'
AND column_name = 'annual_income'
),
'SELECT 1',
'ALTER TABLE `ccdi_staff_fmy_relation` ADD COLUMN `annual_income` DECIMAL(15, 2) NULL COMMENT ''家庭成员年收入(元/年)'' AFTER `mobile_phone2`'
);
PREPARE staff_fmy_relation_annual_income_stmt FROM @staff_fmy_relation_annual_income_sql;
EXECUTE staff_fmy_relation_annual_income_stmt;
DEALLOCATE PREPARE staff_fmy_relation_annual_income_stmt;