Maven+动态SQL(1.27)

一、Maven

1.依赖传递:

Maven 的Dependencies面板中,父级标签(比如org.junit.jupiter:junit-jupiter:5.8.2)是你直接在pom.xml中声明的依赖,而它下面的小标签(比如junit-jupiter-api)是这个依赖自动传递过来的 Jar 包(也就是 “依赖的依赖”)。

image-20260127191549472(1)

  • pom.xml里写了junit-jupiter:5.8.2这个依赖,这是直接依赖

  • junit-jupiter本身需要junit-jupiter-apijunit-jupiter-params等 Jar 包才能运行,这些就是传递依赖,会自动被 Maven 下载并引入。

  • 灰色的 Jar 包:是被<exclusions>排除的传递依赖,不会参与项目编译、运行。如果想恢复这个 Jar 包,只需要删除对应的<exclusion>标签,再刷新 Maven,它就会变回正常颜色~

2.排除依赖:

pom.xml中,给对应的依赖加上<exclusions>标签,排除指定的传递依赖

<!-- 假设这是你直接声明的依赖 -->
<dependency>
    <groupId>cn.wolfcode</groupId>
    <artifactId>Java61_Maven_2</artifactId>
    <version>1.0-SNAPSHOT</version>
    
    <!-- 排除这个依赖传递过来的lombok -->
    <exclusions>
        <exclusion>
            <!-- 要排除的依赖的groupId和artifactId -->
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </exclusion>
    </exclusions>
</dependency>

场景一:若只是不想用传递的依赖(不需要替换版本),则无需后续操作

  • 如果只是单纯不想引入某个传递依赖(比如项目用不到、或避免冲突),不需要重新写 Jar 包,只需要排除即可。

场景二:若需要替换传递依赖的版本(核心场景)

  • 如果传递依赖的版本有问题(比如版本太低 / 太高、有 bug),排除后必须重新声明你需要的版本,否则项目会缺少这个依赖导致报错。

补充说明

  • 排除的是传递依赖(即依赖自动带的 Jar 包),不是你直接声明的依赖;
  • 如果要排除多个传递依赖,可以在<exclusions>里加多个<exclusion>标签。

3.作用范围:

scope 取值 主程序 测试程序 打包(运行) 常见的依赖
compile Y Y Y log4j
test - Y - junit
provided Y Y - servlet-api
runtime - Y Y JDBC驱动

4.更新依赖 索引:

image-20260127194847519(1)

实在不行的话,可以为项目清缓存,缓存失效得重新建立索引

image-20260127195042767(1)

二、MyBatis

1.参数处理:

image-20260127201520333(1)

--这里传参只允许传一个参数,若想传多个参数,则需要用Param注解实现:


(1)传递两个参数的修改流程概述:

测试类修改:从传实体类对象,改为传实体类的两个属性值(如 emp.getId()emp.getName());

image-20260127201823612(1)

Mapper 接口修改:在方法参数前加 @Param 注解,给每个参数命名;

image-20260127203257984(1)

Impl 实现类修改:调用 Mapper 接口方法时,传入这两个独立参数;

image-20260127202444628(1)

XML 修改:去掉 parameterTypeSQL 中直接用 @Param 定义的参数名(如 #{id})。

image-20260127202017343(1)

(2)核心方法:@Param 注解的作用

@Param 注解的核心是给参数 “起名字”,让 MyBatis 能识别多个独立参数 —— 因为 MyBatis 默认只能识别「单个参数」,如果直接传多个参数(如 queryById(Long id, String name)),MyBatis 会把参数封装成 Map,但默认 key 是 arg0/arg1(或 param1/param2),写 SQL 时用 #{arg0} 不直观且易出错。

@Param("id") 后:

  • MyBatis 会把参数封装成 Map,key 是 @Param 里的名称(如 idname);
  • XML 中可以直接用 #{id}/#{name} 引用参数,清晰且不易出错。

(3)为什么要使用 @Param 注解?

根本原因是解决 “多个独立参数的识别问题”

  • 如果不写 @Param,传多个参数时,XML 中只能用 #{arg0}/#{param1} 引用,可读性差;
  • 加了 @Param 后,参数有了明确的名称,XML 中可以用直观的名字(如 #{id}),代码更易维护;
  • 同时避免参数顺序变化导致的错误(比如参数顺序调整后,arg0/arg1 对应的参数会变,但 @Param 名称不变)。

总结来说,@Param 是 MyBatis 中传递多个独立参数的 “简洁方案”,核心作用是给参数命名,让 XML 能清晰识别多个参数

(4)代码拆分解释:

  1. 第一步:获取 Mapper 接口的动态代理对象

    javaEmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
    
    • sqlSession:MyBatis 的核心会话对象,负责与数据库交互;
    • getMapper(EmployeeMapper.class):MyBatis 的动态代理机制,自动生成EmployeeMapper接口的实现类对象(你不需要写手动实现类);
    • 作用:拿到能执行接口方法的代理对象mapper
  2. 第二步:调用接口方法,执行数据库查询

    Employee employee = mapper.queryById(id, name);
    
    • mapper.queryById(id, name):调用代理对象的queryById方法,传入参数idname
    • MyBatis 会自动关联 XML 中id="queryById"<select>标签,执行对应的 SQL 语句;
    • 作用:执行查询,返回Employee实体类对象(对应数据库查询结果)。

(5)OGNL表达式

image-20260127204927637(1)

--左边 mapper.xml 中的 parameterType:代表你传给 MyBatis 的参数(比如一个实体类、Map 或 @Param 封装的参数),它是 OGNL 表达式要操作的数据源。

--右边的 OGNL 表达式结构:用 “树状结构” 表示 OGNL 表达式的执行逻辑,其中 “虚根” 是整个表达式的顶层入口。

(6)什么是 OGNL 表达式?

​ OGNL(Object-Graph Navigation Language)即对象图导航语言,是 MyBatis 动态 SQL 的核心表达式引擎。

  • 核心作用:通过简洁的语法,快速访问 Java 对象的属性、调用方法,判断条件是否成立。

  • 常见用法

    • 在 MyBatis 的 <if test="...">标签中,test 里的条件就是 OGNL 表达式,

    • 比如:

      <if test="ename != null and ename != ''">
          and ename like concat('%', #{ename}, '%')
      </if>
      
    • 它能直接通过属性名(如 ename)访问对象的属性,无需写 getEname()

(7)什么是 OGNL 的 “虚根”?

在 OGNL 的执行机制中,“虚根”(Root Object)是一个顶层的上下文对象,是 OGNL 表达式的 “入口起点”。

  • 作用
    1. 统一入口:所有表达式都从虚根开始导航,访问它包含的属性和方法。
    2. 解决多参数问题:当你用 @Param 传递多个参数时,MyBatis 会把这些参数封装到一个 Map 中,这个 Map 就是 OGNL 的虚根。表达式里的 #{id}#{name} 本质是从这个虚根 Map 中获取值。
    3. 简化访问:如果传递的是单个实体类,这个实体类对象就是虚根,表达式里直接写属性名(如 ename)即可访问。

(8)若只有一个参数,参数的名称是不重要的,因为参数是根

微信图片_20260127233026_128_2

2.#{}与${}的区别:

(1)#{}:15 ’abc‘ --->根据类型添加引号 安全

${}: 15 abc --->原文显示 不安全

(2)总结

  • 日常开发优先用 #{}:它是安全的预编译占位符,能防注入,适合绝大多数场景。
  • 仅在必须动态拼接 SQL 语句时用 ${}:比如动态表名、排序字段等,但必须手动做安全校验。

三、动态Sql

1.复用 SQL 片段:

image-20260127211534002(1)

<sql> 标签:定义可复用的 SQL 片段

<sql id="emp_cols">
    id ,name, sex ,sal
</sql>
  • 作用:把查询时重复使用的列名(id, name, sex, sal)定义成一个可复用的片段,并通过 id="emp_cols" 给它命名。
  • 好处:后续需要修改列名时,只需改这一处,所有引用它的地方都会自动同步,避免了重复修改。

<include> 标签:引用定义好的 SQL 片段

<select id="queryById" resultType="Employee">
    select
    <include refid="emp_cols"/> 
    from employee where id = #{id}
</select>
  • 作用:通过 refid="emp_cols" 引用刚才定义的 SQL 片段,MyBatis 会在运行时把 <include> 替换成 <sql> 里的内容。

2.条件查询:

SELECT 字段1, 字段2, ...
FROM 表名
WHERE 条件1 [AND/OR 条件2] [AND/OR 条件3] ...
[ORDER BY 排序字段 排序方向]
[LIMIT 分页参数];

①新建qo包和EmpQO类:设置需要查询的字段

@Data
@AllArgsConstructor
@NoArgsConstructor
public class EmpQO {
    private String ename;
    private BigDecimal salStart;
    private BigDecimal salEnd;
    private String key;//关键字 多个字段中,只要出现,就符合要求
}

test类:

//条件查询
    @Test
    public void test3() throws ParseException {
        EmpQO qo = new EmpQO();
        //只需要set你想要查询的字段
        //整形字段-->begin and  String-->模糊查询 like '%关键字%' _
        qo.setEname("张三");
        qo.setSalStart(new BigDecimal(2000.0));
        qo.setSalEnd(new BigDecimal(7000.0));
        qo.setKey("三");
        List<Emp> empList = empMapper.queryByCondtion(qo);
        System.out.println(empList);
    }

③Mapper接口:

 int addList(List<Emp> list);

④Mapper实现类:

@Override
    public int addList(List<Emp> list) {
        SqlSession sqlSession = MyBatisTool.getSqlSession();
        int r = sqlSession.insert("cn.wolfcode.mapper.EmpMapper.addList", list);
        sqlSession.commit();
        MyBatisTool.close(sqlSession);
        return r;
    }

⑤xml文件:

<select id="queryByCondtion" resultType="cn.wolfcode.domain.Emp">
        select
            <include refid="emp_cols"/>
        from emp
        <where>
            <if test="ename != null and ename != ''">
                and ename like concat('%',#{ename},'%')
            </if>
            <if test="salStart != null and salStart != ''">
                and sal >= #{salStart}
            </if>
            <if test="salEnd != null and salEnd != ''">
                and sal &lt;= #{salEnd}
            </if>
        </where>
    </select>

where标签的作用:

1. 自动判断是否需要生成 WHERE
  • 如果它包裹的 <if> 条件有任何一个成立,MyBatis 会自动在 SQL 开头加上 WHERE 关键字。
  • 如果所有 <if> 条件都不成立,它就不会生成 WHERE,避免出现 SELECT ... FROM emp WHERE 这种语法错误。

2. 自动处理多余的 AND / OR
  • 当第一个生效的 <if> 条件以 ANDOR 开头时,<where> 会自动把这个多余的前缀去掉。
  • 比如你的代码里每个 <if> 都写了 and ...<where> 会确保最终 SQL 不会出现 WHERE AND ... 这种错误写法。

每个条件都默认带 and,不管它是不是第一个生效的条件,<where> 会帮我们处理掉多余的那个 and

补充:

image-20260127215112795(1)

2.为什么if标签下可以写>=不能写<=?

XML 会把 < 当成标签的开始符号,所以:

  • 在标签的属性值里(比如 <if test="..."> 中的 test 属性),如果直接写 <<=,XML 解析器会误以为是新标签的开始,导致解析报错。
  • 在标签的内容区(比如 <if> 标签内部的 SQL 代码),>= 可以直接写,因为 > 不会被误解析;但 <= 里的 < 仍然会触发解析错误,所以必须转义。

3.批量增加:

INSERT INTO 表名 (字段1, 字段2, 字段3, ...) 
VALUES 
(值1-1, 值1-2, 值1-3, ...),  -- 第一条数据 
(值2-1, 值2-2, 值2-3, ...),  -- 第二条数据 
(值3-1, 值3-2, 值3-3, ...);  -- 第三条数据(最后一条末尾只写分号)

test类:

 EmpMapper empMapper = new EmpMapperImpl();
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
    //批量添加
    @Test
    public void test() throws ParseException {
        Date date = sdf.parse("2026-01-26");
        List<Emp> list = List.of(
                new Emp(1001L,"张三","摸鱼",7839L,date,6000.0,1000.0,20L),
                new Emp(1002L,"李四","看小说",7839L,date,7000.0,1500.0,20L),
                new Emp(1003L,"王五","睡觉",7839L,date,8000.0,2000.0,20L)
        );
        int r = empMapper.addList(list);
        if(r>0){
            System.out.println("添加成功!");
        }else{
            System.out.println("添加失败!");
        }
    }

②mapper接口:

 int addList(List<Emp> list);

③mapper实现类:

   @Override
    public int addList(List<Emp> list) {
        SqlSession sqlSession = MyBatisTool.getSqlSession();
        int r = sqlSession.insert("cn.wolfcode.mapper.EmpMapper.addList", list);
        sqlSession.commit();
        MyBatisTool.close(sqlSession);
        return r;
    }

④xml文件:

<insert id="addList">
	<!-- 1. emp 是数据库表名 -->
	insert into emp (
 	<!-- 2. 括号内的都是数据库表的字段名 -->
  	empno,ename,job,mgr,hiredate,sal,comm,deptno
	) values
	<!-- 3. list 是方法传入的List集合参数名 -->
	<foreach collection="list" 
         <!-- 4. emp 是遍历List时单个Emp对象的别名 -->
         	item="emp" 
         	separator=",">
    	<!-- 5. emp.empno/ename/... 是Emp实体类的属性名 -->
    	(#{emp.empno},#{emp.ename},#{emp.job},#{emp.mgr},#{emp.hiredate},#{emp.sal},#{emp.comm},#					{emp.deptno})
	</foreach>
</insert>

1. collection="list"

  • 作用:指定你要遍历的数据源(集合)名称

  • 具体含义

    • 这里的 list 是 MyBatis 的默认值—— 当你的 Mapper 接口方法参数是一个 List 集合(且没有加 @Param 注解命名)时,MyBatis 会默认把这个集合命名为 list,所以 <foreach> 要通过 collection="list" 找到这个集合。
    • 举个例子:如果你的接口方法是 int addList(List<Emp> empList);,因为参数是 List 且无 @Param,所以 XML 里必须写 collection="list"
    • 补充:如果接口加了 @Param 注解(比如 int addList(@Param("empList") List<Emp> empList);),那 collection 就要改成 empList,和注解里的名称一致。

2. item="emp"

  • 作用:给遍历集合时的单个元素起别名这个别名和你自己建的 Emp 类没有直接关联,不需要和 Emp 类名一致

    真正需要一致的是什么?

    唯一需要和 Emp 对象一致的,是 #{别名.属性名} 里的属性名,而不是别名本身:

  • 具体含义

    • 遍历 list 这个集合时,每循环一次会取出一个 Emp 对象,item="emp" 就是给这个 “当前取出的 Emp 对象” 起个名字叫 emp
    • 后续 #{emp.empno}#{emp.ename} 里的 emp,就是引用这个别名,用来获取单个 Emp 对象的属性值。
    • 类比理解:就像 Java 里的增强 for 循环 for (Emp emp : empList),这里的 item="emp" 对应循环里的 Emp empcollection="list" 对应 empList

拓展:

openclose 属性的作用

属性 作用
open 指定 <foreach> 遍历内容开头要拼接的字符串
close 指定 <foreach> 遍历内容结尾要拼接的字符串

例如:

insert into emp (
  empno,ename,job,mgr,hiredate,sal,comm,deptno
) values
<foreach collection="list" item="emp" separator=", open="(" close=")">
    #{emp.empno},#{emp.ename},#{emp.job},#{emp.mgr},#{emp.hiredate},#{emp.sal},#{emp.comm},#{emp.deptno}
</foreach>

4.批量删除:

DELETE FROM 表名 
WHERE 主键字段 IN (值1, 值2, 值3, ...);

test类:

 //批量删除
    @Test
    public void test1() {
        Long [] ids = {1001L,1002L,1003L};
        int r = empMapper.deleteByIds(ids);
        if(r>0){
            System.out.println("删除成功!");
        }else{
            System.out.println("删除失败!");
        }
    }

②mapper接口:

int deleteByIds(Long[] ids);

③mapper实现类:

@Override
    public int deleteByIds(Long[] ids) {
        SqlSession sqlSession = MyBatisTool.getSqlSession();
        int r = sqlSession.delete("cn.wolfcode.mapper.EmpMapper.deleteByIds", ids);
        sqlSession.commit();
        MyBatisTool.close(sqlSession);
        return r;
    }

④xml文件:

<delete id="deleteByIds">
        delete from emp
        where empno in (
            <foreach collection="array" item="empno" separator=",">
                #{empno}
            </foreach>
        )
</delete>

1.collection="array" 的含义

  • 核心作用:指定你要遍历的数据源(参数)名称

  • 具体解释:

    1. array 是 MyBatis 的默认关键字—— 当你的 Mapper 接口方法参数是一个数组(比如 int[] empnosInteger[] empnos),且没有加 @Param 注解命名时,MyBatis 会默认把这个数组参数命名为 array,所以 <foreach> 要通过 collection="array" 找到这个数组。
    2. 举个例子:如果你的接口方法是 int deleteByIds(Integer[] empnos);,因为参数是数组且无 @Param,所以 XML 里必须写 collection="array";如果加了注解(比如 int deleteByIds(@Param("empnos") Integer[] empnos);),那 collection 就要改成 empnos

2.item="empno" 的含义

  • 核心作用:给遍历数组时的单个元素起别名
  • 具体解释:
    1. 遍历 array 这个数组时,每循环一次会取出一个数组元素(比如 1001、1002、1003),item="empno" 就是给这个 “当前取出的单个主键值” 起个名字叫 empno
    2. 后续 #{empno} 里的 empno,就是引用这个别名,用来获取数组中单个的员工编号值。
    3. 类比 Java 循环:就像 for (Integer empno : empnos) {},这里的 item="empno" 对应循环里的 Integer empnocollection="array" 对应 empnos 数组。

注意区分:

  • 遍历基本类型(数组 / 集合):元素本身就是目标值,直接写 #{别名}(比如 #{empno});

  • 遍历实体类对象(集合):元素是对象,需要写 #{别名.属性名}(比如 #{emp.empno})。

5.局部更新:字段为null-->不更新 字段为null-->更新

UPDATE 表名
SET 
  字段1 = 值1,
  字段2 = 值2,
  -- 只写需要更新的字段,不需要的字段不写
WHERE 
  更新条件;

test类:

//局部更新
@Test
public void test2() throws ParseException {
    Date date = sdf.parse("2026-01-27");
    Emp emp = new Emp(1000L,"张三","摸鱼",7839L,date,null,null,null);
    int r = empMapper.updatePartten(emp);
    if(r>0){
        System.out.println("更新成功!");
    }else{
        System.out.println("更新失败!");
    }
}

②mapper接口:

int updatePartten(Emp emp);

③mapper实现类:

@Override
public int updatePartten(Emp emp) {
    SqlSession sqlSession = MyBatisTool.getSqlSession();
    int r = sqlSession.delete("cn.wolfcode.mapper.EmpMapper.updatePartten", emp);
    sqlSession.commit();
    MyBatisTool.close(sqlSession);
    return r;
}

④xml文件:

<update id="updatePartten">
        update emp
        <set>
            <!-- 普通字符串/数值字段:可以判断 != '' and != null -->
            <if test="ename != null and ename !=''">
                ename = #{ename},
            </if>
            <if test="job != null and job !=''">
                job = #{job},
            </if>
            <if test="mgr != null and mgr !=''">
                mgr = #{mgr},
            </if>
            <if test="hiredate != null">
                hiredate = #{hiredate},
            </if>
        </set>
        where empno = #{empno}
    </update>

1.补充:末尾的逗号为什么不会报错?

你可能注意到这行末尾有个逗号 ,,但不用担心语法错误:

<set> 标签会自动处理多余的逗号 —— 比如最终只有 hiredate 字段要更新时,<set> 会把 hiredate = #{hiredate}, 末尾的逗号去掉,生成 set hiredate = ?,避免 SQL 语法错误。

2.<set> 标签的核心作用

(1) 自动处理多余的逗号(最关键)

在动态更新时,每个 <if> 里的更新字段末尾都会加逗号(比如 ename = #{ename},),如果最后一个生效的字段也带逗号,会生成 SET ename = '张三', 这种语法错误的 SQL。

<set> 会自动识别并删除最后一个字段后面多余的逗号,保证 SQL 语法正确。

(2) 动态控制 SET 关键字的生成

  • 如果 <set> 包裹的 <if> 有任意一个条件成立(有字段要更新),MyBatis 会自动在 SQL 开头加上 SET 关键字;
  • 如果所有 <if> 条件都不成立(没有字段要更新),<set>不会生成 SET,避免出现 UPDATE emp SET WHERE empno = ? 这种语法错误。

6.RequestMap标签:

(1)清核心问题:为什么需要 resultMap

resultType="Employee" 能直接映射的前提是:数据库表字段名 = 实体类属性名(比如表字段 emp_name ≠ 实体类 empName 就会映射失败);如果有计算字段(比如 sal*12 as annual_sal),resultType 也无法直接映射到实体类的 annualSal 属性。

(2)解决两个小问题:

1. 字段名不一致:两种解决方案

方案 1:SQL 取别名(临时解决,适合简单场景)

如果表字段是 emp_name,实体类属性是 empName,可以在 SQL 里给字段取别名,让别名和属性名一致:

<select id="getEmpById" resultType="Employee">
    <!-- 表字段emp_name → 别名empName(和实体类属性一致) -->
    select empno, emp_name as empName, job, sal from emp where empno = #{empno}
</select>
方案 2:resultMap 映射(推荐,一劳永逸)

如果多个查询都需要映射,不用每次写别名,直接定义 resultMap

<!-- 1. 定义resultMap:给MyBatis写映射规则 -->
<resultMap id="EmpResultMap" type="Employee">
    <!-- id:映射主键字段(可选,但推荐写) -->
    <id column="empno" property="empno"/>
    <!-- result:映射普通字段 → column=表字段名,property=实体类属性名 -->
    <result column="emp_name" property="empName"/>
    <result column="job" property="job"/>
    <result column="sal" property="sal"/>
</resultMap>

<!-- 2. 查询时引用resultMap -->
<select id="getEmpById" resultMap="EmpResultMap">
    <!-- SQL不用取别名,直接写表字段名 -->
    select empno, emp_name, job, sal from emp where empno = #{empno}
</select>

2. 有计算字段:必须用 resultMap(或别名 +resultType

比如要计算年薪(sal*12),实体类有 annualSal 属性,两种写法:

方案 1:SQL 别名 + resultType(临时)
<select id="getEmpAnnualSal" resultType="Employee">
    <!-- 计算字段取别名,和实体类annualSal一致 -->
    select empno, emp_name as empName, sal, sal*12 as annualSal from emp where empno = #{empno}
</select>
方案 2:resultMap(推荐,计算字段也能映射)
<!-- 定义resultMap:新增计算字段的映射 -->
<resultMap id="EmpWithAnnualSalMap" type="Employee">
    <id column="empno" property="empno"/>
    <result column="emp_name" property="empName"/>
    <result column="sal" property="sal"/>
    <!-- column=SQL里的计算字段别名,property=实体类属性 -->
    <result column="annual_sal" property="annualSal"/>
</resultMap>

<!-- 查询时SQL写计算字段,不用和属性名一致 -->
<select id="getEmpAnnualSal" resultMap="EmpWithAnnualSalMap">
    select empno, emp_name, sal, sal*12 as annual_sal from emp where empno = #{empno}
</select>

(3)resultMap 详细拆解(易懂版)

1. resultMap 核心结构

<!-- id:resultMap的唯一标识(查询时要引用);type:要映射的实体类全类名(或别名) -->
<resultMap id="自定义ID" type="实体类全类名/别名">
    <!-- 1. 主键映射(可选,标记主键字段,提升性能) -->
    <id column="数据库表字段名/计算字段别名" property="实体类属性名"/>
    
    <!-- 2. 普通字段映射(核心) -->
    <result column="数据库表字段名/计算字段别名" property="实体类属性名"/>
    
    <!-- 3. 高级用法:关联查询/集合查询(后续学联表时用) -->
    <association/><!-- 一对一关联,比如员工关联部门 -->
    <collection/><!-- 一对多关联,比如部门关联多个员工 -->
</resultMap>

2. 核心参数说明(关键!)

参数 含义
id(resultMap 的属性) 给这个映射规则起个名字,查询时用 resultMap="这个名字" 引用
type 要映射的实体类(比如 com.xxx.pojo.Employee,或配置别名后写 Employee
column 数据库查询结果里的 “列名”(可以是表原字段名、SQL 别名、计算字段别名)
property 实体类里的 “属性名”(比如 empNameannualSal
posted on 2026-01-27 23:33  冬冬咚  阅读(6)  评论(0)    收藏  举报