news 2026/9/18 23:26:52

Spring Boot人事管理系统:数据模型、薪资批算与并发控制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot人事管理系统:数据模型、薪资批算与并发控制

简介:本资源为一份基于Java的人事管理系统设计与实现毕业论文文档,面向高校计算机相关专业的本科生、课程设计者及需要参考完整开发流程的初学者。文档围绕工作人员的统一管理展开,涵盖录入、查询、删除、修改等核心操作,并采用Java语言配合Microsoft Access数据库完成开发。压缩包共包含1个doc文件,约982KB,内容为完整的论文正文,含绪论、需求分析、系统设计、数据库设计、详细设计、系统调试与总结等章节,涉及Java语言特性、MyEclipse开发环境、功能结构图、E-R图以及登录界面、基础信息管理、人员调动、人员考核、劳资管理等模块的具体实现描述,可作为论文写作与系统开发的参照模板。目前已有681人学习下载,适合用于梳理从需求分析到测试上线的整体思路,并借鉴其模块划分与数据库设计方法。

1. 为什么一份人事管理系统需求,最后都会落到 Java 后端

很多团队接人事管理系统的活,第一反应是先画十几个页面,等页面画完才发现员工、组织、岗位、合同、薪资之间的关系根本理不清。人事管理系统真正的难点不在界面,而在于「同一个人在不同时间点属于不同部门、拿不同薪资、走不同审批流」,这种带时间维度的数据一致性,才是决定系统能不能上线的分水岭。Java 生态在这类场景里的家底足够厚:Spring Boot 负责装配,MyBatis 或 JPA 负责持久化,MySQL 存主数据,Redis 扛会话和字典缓存,JDK 自带的LocalDateBigDecimalCompletableFuture又刚好覆盖日期、金额和批量计算这三个高频痛点。对刚补完 java 基础的同学,这套组合是能照着跑通的;对写了几年业务的后端,边界在哪、坑在哪也都有迹可循。下面按数据模型、核心模块、并发与一致性、上线验证四段推进,每段都给能直接抄的代码和参数。

2. 人事管理系统的数据模型怎么定:员工主档、组织树与任职记录

模型定错了,后面写多少 CRUD 都是白干。人事系统最常见的返工,是把「人」和「岗位」直接绑死在一张表里,结果员工一调岗,历史薪资和考勤全跟着变。先把概念拆开,再落表结构。

2.1 先分清「人」「岗」「任职」三张概念表

把员工(person)当成一个自然人主档,工号、姓名、证件、入职日期这些属性一辈子基本不变;岗位(position)是组织里的编制位;任职记录(employment)才是把人和岗位连起来的那根线,并且带起止时间。三者拆开之后,「张三 2023 年在研发部做后端、2024 年调到架构组」就是两条任职记录,而不是一次覆盖写。

表名承载什么关键字段是否随时间变化
person自然人主档emp_no、name、id_card_hash、hire_date、status基本不变
department组织节点id、name、parent_id、path、leader_id会调整
position岗位编制id、dept_id、title、grade会调整
employment任职关系person_id、dept_id、position_id、start_date、end_date核心时间维度
salary_record薪资结果person_id、month、gross、net、batch_id按月固化

这张表里最关键的一条经验是:任何「会随时间变化的关系」都不要写成主档上的一个字段,否则历史数据永远对不上账。

2.2 员工主档与组织树的建表 SQL

下面这段是在 MySQL 8 上能直接执行的最小结构,字符集统一 utf8mb4,金额字段一律DECIMAL,绝不用FLOAT

CREATE TABLE `person` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `emp_no` VARCHAR(32) NOT NULL COMMENT '工号,全局唯一', `name` VARCHAR(64) NOT NULL COMMENT '姓名', `id_card_hash` CHAR(64) NOT NULL COMMENT '证件号哈希,避免明文落库', `hire_date` DATE NOT NULL COMMENT '首次入职日期', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1在职 2离职 3待入职', `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标记', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_emp_no` (`emp_no`), KEY `idx_hire_date` (`hire_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='员工主档'; CREATE TABLE `employment` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `person_id` BIGINT UNSIGNED NOT NULL, `dept_id` BIGINT UNSIGNED NOT NULL, `position_id` BIGINT UNSIGNED NOT NULL, `start_date` DATE NOT NULL COMMENT '任职生效日', `end_date` DATE NOT NULL DEFAULT '9999-12-31' COMMENT '失效日,用极大值代替 NULL', `primary_flag` TINYINT NOT NULL DEFAULT 1 COMMENT '1主职 0兼职', PRIMARY KEY (`id`), KEY `idx_person_period` (`person_id`, `start_date`, `end_date`), KEY `idx_dept` (`dept_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='任职记录';

end_date不给 NULL 而给9999-12-31,是为了让区间查询统一写成start_date <= d AND end_date >= d,不用到处判空,BETWEEN也用不了 NULL。id_card_hash存哈希而不是明文,是合规底线,真要做校验再加一层加盐。

2.3 组织树查询:邻接表 + 递归 CTE 够不够用

组织树 90% 的场景只有两种查询:查某个部门的所有下级、查某人所在部门的完整路径。邻接表(只存parent_id)配合 MySQL 8 的递归 CTE 就能扛住千级节点。

2.3.1 递归 CTE 的标准写法
WITH RECURSIVE dept_tree AS ( SELECT id, name, parent_id, 1 AS lvl FROM department WHERE parent_id = 0 -- 根节点,按实际约定填 UNION ALL SELECT d.id, d.name, d.parent_id, t.lvl + 1 FROM department d JOIN dept_tree t ON d.parent_id = t.id ) SELECT id, name, lvl FROM dept_tree ORDER BY lvl, id;

lvl是层级,方便前端渲染缩进;UNION ALL不能写UNION,否则会去重排序,几千节点时差距很明显。注意 MySQL 默认cte_max_recursion_depth是 1000,组织层级一般不超过 10,但如果你的代码里出现了自引用死循环(比如 A 的父节点是 B、B 的父节点是 A),这条 SQL 会一直递归到报错,所以维护部门时必须在服务层校验「新父节点不能是自己的子孙」。

2.3.2 什么时候该换成闭包表或路径枚举

如果需求里出现「按层级统计人数、按任意祖先筛选」,递归 CTE 就有点吃力了,这时候加一张闭包表dept_closure(ancestor_id, descendant_id, depth),插入时同步维护,查询变成一次普通 JOIN。取舍参考下表。

方案查询上级查询下级移动部门适用规模
邻接表递归 CTE递归 CTE只改一行千级节点
路径枚举(path)LIKE 'a/b/%'LIKE前缀批量改子节点 path百级节点
闭包表一次 JOIN一次 JOIN删插若干行万级节点、读多写少

多数人事系统停在邻接表就够了,别为了「看起来专业」提前上闭包表,维护成本会翻倍。

2.4 任职记录为什么要带生效区间

薪资和考勤都是按月跑批的,跑批时必须能回答「这个月这个人属于哪个部门、哪个岗位」。有了start_dateend_date,查询就是一句:

SELECT e.person_id, e.dept_id, e.position_id FROM employment e WHERE e.person_id = #{personId} AND e.start_date <= #{monthEnd} AND e.end_date >= #{monthStart} ORDER BY e.primary_flag DESC, e.start_date DESC LIMIT 1;

同一个人同一时间段理论上只应有一条主职记录,这条约束别只写在文档里,用一条唯一索引近似实现:在employment上加UNIQUE KEY uk_person_start (person_id, start_date, primary_flag),能挡住大部分重复提交,剩下的跨区间重叠交给服务层校验。

2.5 逻辑删除与唯一索引的冲突怎么解

deleted标记加UNIQUE KEY (emp_no)会打架:员工离职后重新入职,工号复用就插不进去。常见做法有两种,一是把唯一索引改成(emp_no, deleted),但deleted只有 0/1,只能允许一条删除记录;二是删除时把emp_no改写成原工号_时间戳,保留原始值到另一个字段。我一般选第二种,因为离职返聘在制造业和零售业是常态,工号复用是刚需,第一种方案第二次复用就会撞唯一键。

3. 基于 Spring Boot 的人事管理系统核心模块实现

模型确定后,编码阶段的目标是:接口能跑、参数能校验、权限能兜底、规则能扩展。以下按骨架、CRUD、数据权限、薪资规则四块展开。

3.1 用 start.spring.io 拉一个能跑的最小骨架

JDK 装好后先确认java -versionJAVA_HOME,环境变量没配对,后面 Maven 编译报的错会一路迷惑你。骨架直接用官方初始化器拉,比手写 pom 少踩一半依赖坑。

# 版本号按本地 JDK 环境替换,JDK 17 对应 Spring Boot 3.x curl -s https://start.spring.io/starter.zip \ -d type=maven-project \ -d language=java \ -d javaVersion=17 \ -d groupId=com.example \ -d artifactId=hrms \ -d dependencies=web,mybatis,mysql,validation,lombok \ -o hrms.zip unzip hrms.zip -d hrms cd hrms && ./mvnw spring-boot:run

关键依赖四个:web提供 REST,mybatis做持久化,mysql是驱动,validation管参数校验。启动类上记得加@MapperScan("com.example.hrms.mapper"),不加的话 MyBatis 的接口不会被扫描,启动时不报错,调接口时才抛Invalid bound statement,这个错排查起来最浪费时间。

application.yml里连接池参数给一组起步值:

spring: datasource: url: jdbc:mysql://127.0.0.1:3306/hrms?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true username: hrms password: ${DB_PASSWORD} hikari: maximum-pool-size: 20 # 按 QPS 估,一般 10~20 够用 minimum-idle: 5 connection-timeout: 3000 max-lifetime: 1800000 # 略小于 MySQL wait_timeout

rewriteBatchedStatements=true这条一定要加,它让批处理 INSERT 合并成多值语句,跑薪资初始化数据时性能差好几倍。

3.2 员工创建接口:参数校验别交给前端

服务端校验是最后一道闸,缺了它,脏数据会一路流到薪资表里。

@RestController @RequestMapping("/api/employees") @Validated public class EmployeeController { private final EmployeeService employeeService; public EmployeeController(EmployeeService employeeService) { this.employeeService = employeeService; } @PostMapping public Result<Long> create(@RequestBody @Valid EmployeeCreateReq req) { // 业务级校验:入职日期不允许晚于今天 if (req.getHireDate().isAfter(LocalDate.now())) { throw new BizException("入职日期不能晚于今天"); } return Result.ok(employeeService.create(req)); } } @Data public class EmployeeCreateReq { @NotBlank(message = "工号不能为空") @Pattern(regexp = "^[A-Z0-9]{4,32}$", message = "工号格式不合法") private String empNo; @NotBlank(message = "姓名不能为空") @Size(max = 64) private String name; @NotNull(message = "入职日期不能为空") private LocalDate hireDate; @NotNull(message = "部门不能为空") private Long deptId; }

校验分两层:注解层负责格式和空值,@Valid触发;业务层负责跨字段和跨表规则,比如工号是否已存在、部门是否存在。两层职责别混,注解里写不进数据库查询。日期和金额用java.time.LocalDateBigDecimal这两个常用类,前者避开SimpleDateFormat的线程安全问题,后者避开double的精度丢失。

3.3 MyBatis 分页与动态条件

列表查询最忌讳SELECT *加一串if拼 SQL。MyBatis 的动态标签配合理赔参数对象更清爽。

<select id="pageQuery" resultType="com.example.hrms.vo.EmployeeVO"> SELECT p.id, p.emp_no, p.name, p.hire_date, p.status, d.name AS deptName, pos.title AS positionTitle FROM person p LEFT JOIN employment e ON e.person_id = p.id AND e.end_date >= CURDATE() <!-- 只取当前生效的任职 --> LEFT JOIN department d ON d.id = e.dept_id LEFT JOIN position pos ON pos.id = e.position_id WHERE p.deleted = 0 <if test="name != null and name != ''"> AND p.name LIKE CONCAT('%', #{name}, '%') </if> <if test="deptId != null"> AND e.dept_id IN <foreach collection="subDeptIds" item="id" open="(" separator="," close=")"> #{id} </foreach> </if> ORDER BY p.hire_date DESC </select>

两个要点:一是任职表用ON里的区间条件做过滤,别搬到WHERE,否则LEFT JOIN会退化成INNER JOIN,没任职记录的员工直接查不出来;二是部门筛选走subDeptIds,由服务层先查出所有下级部门 id 再传进来,不让 SQL 里写递归。

3.4 数据权限用 AOP 切面,底层就是 JDK 动态代理

HR 系统里,「部门主管只能看本部门及下级员工」是最常被面试和实战双重拷打的点。实现方式我一般用注解加切面,把数据范围塞进ThreadLocal,MyBatis 拦截器再读出来拼 SQL。

@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface DataScope { String deptAlias() default "e"; } @Aspect @Component public class DataScopeAspect { @Around("@annotation(dataScope)") public Object around(ProceedingJoinPoint pjp, DataScope dataScope) throws Throwable { LoginUser user = CurrentUserHolder.get(); // 超级管理员不加范围,普通用户限定到自己所在部门的子树 if (!user.isAdmin()) { List<Long> scope = deptService.listSubDeptIds(user.getDeptId()); DataScopeContext.set(scope); } try { return pjp.proceed(); } finally { DataScopeContext.clear(); // 必须清理,线程池复用会串上下文 } } }

Spring AOP 对实现了接口的 Bean 默认走 JDK 动态代理,对没有接口的类退化成 CGLIB 子类代理。这带来两个实际约束:被切的方法不能是privatefinal,同类内部方法直接调用也不会触发切面。ThreadLocal一定要在finally里清,Tomcat 线程是复用的,不清就等于把上一个请求的可见范围泄给下一个请求,这是权限系统里最隐蔽的事故。

3.5 薪资规则用策略模式替代 if-else

固定薪、时薪、计件、提成,一开始都是 if-else,加两条规则后就没法看了。把每条规则做成一个实现类,用Map收集。

public interface SalaryRule { String type(); BigDecimal calc(SalaryContext ctx); } @Component public class HourlySalaryRule implements SalaryRule { public String type() { return "HOURLY"; } public BigDecimal calc(SalaryContext ctx) { // 月计薪天数 21.75,日工作 8 小时 BigDecimal hourly = ctx.getBase() .divide(new BigDecimal("21.75"), 4, RoundingMode.HALF_UP) .divide(new BigDecimal("8"), 4, RoundingMode.HALF_UP); return hourly.multiply(BigDecimal.valueOf(ctx.getWorkHours())) .setScale(2, RoundingMode.HALF_UP); } } @Component public class SalaryRuleRegistry { private final Map<String, SalaryRule> rules = new HashMap<>(); public SalaryRuleRegistry(List<SalaryRule> ruleList) { ruleList.forEach(r -> rules.put(r.type(), r)); } public BigDecimal calc(String type, SalaryContext ctx) { SalaryRule rule = rules.get(type); if (rule == null) throw new BizException("未知薪资规则: " + type); return rule.calc(ctx); } }

新增规则只需要加一个@Component类,不动任何已有代码,这就是开闭原则的落地方式。所有除法统一指定精度和舍入模式,BigDecimal不指定RoundingMode遇到无限小数会直接抛ArithmeticException

4. 考勤汇总、薪资批算与并发控制怎么做

到了跑批环节,性能和数据一致性同时压过来。这一章处理四件事:批量写、并发取数、批次幂等、缓存正确性。

4.1 批量更新为什么不能循环单条 update

一千人循环update,就是一千次网络往返加一千次事务日志刷盘。正确的做法是分批拼接,配合前面开的rewriteBatchedStatements

@Transactional(rollbackFor = Exception.class) public void batchSaveSalary(List<SalaryRecord> records) { int batchSize = 500; for (int i = 0; i < records.size(); i += batchSize) { List<SalaryRecord> sub = records.subList(i, Math.min(i + batchSize, records.size())); salaryMapper.batchInsert(sub); // 单条 INSERT ... VALUES (...),(...) } }

批大小给 500 是个折中:太小网络往返多,太大单条 SQL 超过max_allowed_packet(默认 4MB)会直接报错,而且锁持有时间变长。分批之后整体事务不要太大,几千条以上建议拆到多个事务,失败重跑依赖 4.3 的幂等设计。

4.2 CompletableFuture 并发汇总考勤,等全部完成后合并

考勤汇总要调多个数据源或者多次查库,串行跑一千人可能要几十秒。可以把每人一个任务丢进线程池,用allOf等全部完成后统一取结果。

private final ExecutorService ioPool = new ThreadPoolExecutor( 8, 16, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(200), // 有界队列,防止任务堆积 OOM new ThreadFactoryBuilder().setNameFormat("att-sum-%d").build(), new ThreadPoolExecutor.CallerRunsPolicy()); // 满了让提交线程自己跑,起到削峰作用 public List<AttendanceSummary> summarize(Long month, List<Long> personIds) { List<CompletableFuture<AttendanceSummary>> futures = personIds.stream() .map(id -> CompletableFuture .supplyAsync(() -> attendanceService.summary(id, month), ioPool) .exceptionally(ex -> AttendanceSummary.empty(id, month))) // 单人失败不拖垮整批 .toList(); // 阻塞直到全部任务结束;allOf 完成后 join 不会再次阻塞 CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); return futures.stream() .map(CompletableFuture::join) .toList(); }

allOf(...).join()是等的核心,它只在全部完成后返回;后续join()取的是已完成结果,不会阻塞。exceptionally必须加,否则一个员工的数据异常会让整批 join 抛CompletionException,前面成功的部分全白跑。

线程池参数建议:

参数建议值依据
corePoolSize8任务以查库为主,IO 等待占比高
maxPoolSize16峰值不超过数据库连接池的 80%
queueCapacity200有界队列,拒绝时走调用方线程
keepAliveTime60s跑批是周期性峰值,空闲收缩
拒绝策略CallerRunsPolicy不丢任务,反向压住提交速度

线程池大小别拍脑袋超过 Hikari 的maximum-pool-size,否则任务全在等连接,线程数再多也没用。

4.3 薪资批次状态机与幂等键

跑批最怕的是重复执行,第二次跑出来的金额覆盖了第一次人工调过的数据。用一个显式的批次状态机管住流程。

状态含义允许流转到说明
DRAFT草稿,可调参数CALCULATING允许改薪资规则
CALCULATING计算中DONE / FAILED加分布式锁,防并发触发
DONE计算完成待复核CONFIRMED / DRAFT复核驳回可退回草稿
CONFIRMED已确认PAID已确认金额不再允许脚本覆盖
PAID已发放终态

同时给salary_recordUNIQUE KEY uk_person_month (person_id, month, batch_id),重复跑批时用INSERT ... ON DUPLICATE KEY UPDATE只更新未确认的数据,已CONFIRMED的记录在 SQL 的WHERE里直接排除。这样即使运维手动重跑,也不会动到已发放的账。

4.4 字典缓存用 Redis,重点在防穿透和过期策略

部门树、岗位列表这类字典读多写少,适合放 Redis。但缓存写不对,反而制造 bug:穿透、雪崩、脏读,三个里最容易踩的是穿透。

public Department getDept(Long id) { String key = "hr:dept:" + id; String cached = redis.opsForValue().get(key); if (cached != null) { // 空字符串是穿透占位符,直接返回 null 不再查库 return cached.isEmpty() ? null : JSON.parseObject(cached, Department.class); } Department dept = deptMapper.selectById(id); if (dept == null) { // 不存在的 key 也缓存,但过期时间要短,防止脏数据长期占用 redis.opsForValue().set(key, "", 60, TimeUnit.SECONDS); return null; } // 加随机偏移,避免同一时刻批量过期造成雪崩 long ttl = 30 * 60 + ThreadLocalRandom.current().nextInt(300); redis.opsForValue().set(key, JSON.toJSONString(dept), ttl, TimeUnit.SECONDS); return dept; }

过期时间加随机偏移这条经常被忽略,部门字典如果整点统一重建,整批 key 会在同一秒失效,请求全砸到数据库。写操作更新缓存时,用「先更新库、再删缓存」而不是「更新缓存」,删除失败比更新成旧值更好修。

4.5 事务失效的三个高频坑

第一个是同类内部方法调用,this.batchSave()走不到代理,事务注解形同虚设,要拆到别的 Bean 或者注入自己。第二个是异常被吞掉,catch完没重新抛,@Transactional默认只对RuntimeException回滚,受检异常要显式写rollbackFor = Exception.class。第三个是@Transactional方法里开了新线程,子线程的事务和主线程完全无关,主线程回滚不会带走子线程已经提交的数据,所以 4.2 里的批处理必须保证每个任务是幂等的,不能指望外层事务兜底。

5. 上线前的验证:初始化数据、慢 SQL 与容器化部署

代码跑通只算一半,交付前有几件事必须过一遍,否则上线当天最容易翻车。

5.1 初始化数据与冒烟脚本

交付给客户的环境,第一条 SQL 不是业务数据,而是基础字典和第一个管理员账号。

# 1. 建库建表 mysql -h127.0.0.1 -uroot -p hrms < schema.sql # 2. 灌字典和根部门 mysql -h127.0.0.1 -uroot -p hrms < init_data.sql # 3. 冒烟:建一个员工再查出来,验证写读链路 curl -s -X POST http://127.0.0.1:8080/api/employees \ -H 'Content-Type: application/json' \ -d '{"empNo":"E10001","name":"测试员工","hireDate":"2024-01-02","deptId":1}' curl -s 'http://127.0.0.1:8080/api/employees/page?pageNum=1&pageSize=10'

冒烟脚本要进 CI,别只在自己机器上手工点两下。返回值里code不是 0 就中断,比人工看日志可靠。

5.2 盯住三条必查的慢 SQL

人事系统里最容易慢的不是列表,而是跨表统计。列表超过 1 秒,先EXPLAIN看三处:

-- 1. 员工列表:确认走了 uk_emp_no 或 idx_hire_date,不要出现 type=ALL EXPLAIN SELECT p.id FROM person p WHERE p.deleted = 0 ORDER BY p.hire_date DESC LIMIT 20; -- 2. 部门人数统计:确认 idx_dept 生效,避免全表扫 employment EXPLAIN SELECT dept_id, COUNT(*) FROM employment WHERE end_date >= CURDATE() GROUP BY dept_id; -- 3. 薪资区间查询:确认走 (person_id, month) 复合索引 EXPLAIN SELECT * FROM salary_record WHERE person_id = 10086 AND month BETWEEN '2024-01' AND '2024-06';

看到Using filesort且行数上万,就补索引;看到type=ALL就检查是不是条件里对索引列做了函数运算,比如DATE_FORMAT(hire_date,'%Y') = '2024',这种写法会让索引直接失效,改成hire_date >= '2024-01-01' AND hire_date < '2025-01-01'

5.3 容器化部署与最后的交付细节

容器里跑 Java 应用有两个参数必须显式给,否则容器看到的是宿主机内存,JVM 会按物理机内存开堆。

FROM eclipse-temurin:17-jre WORKDIR /app COPY target/hrms.jar app.jar # 让 JVM 感知容器内存上限,堆占容器上限的 75% ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75.0", \ "-XX:+HeapDumpOnOutOfMemoryError", \ "-Duser.timezone=Asia/Shanghai", \ "-jar", "app.jar"]

-Duser.timezone这条务必加,容器默认 UTC,考勤跨天判定会在凌晨出错,这个 bug 上线后极难复现。交付前最后两件事:一是把数据库账号密码走环境变量而不是写进 yml,二是如果交付的是可反编译的 jar,评估一下是否需要字节码混淆,至少把连接串和许可校验相关的常量处理掉,别裸奔。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!