Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
import jakarta.validation.constraints.Size;

public record RegisterRequest(
@NotBlank @Size(max = 40) String username,
@NotBlank @Size(max = 100) String password,
@NotBlank @Size(max = 100) String confirmPassword
@NotBlank @Size(min = 3, max = 40) String username,
@NotBlank @Size(min = 6, max = 100) String password,
@NotBlank @Size(min = 6, max = 100) String confirmPassword
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,14 @@ public ApiResponse<Void> delete(@PathVariable Long id) {
return ApiResponse.ok(null);
}

@PostMapping("/contracts/{id}/cancel")
@RequirePermission("contract:delete")
public ApiResponse<Void> cancel(@PathVariable Long id) {
SysUser user = authService.requireUser();
contractService.cancel(id, user);
return ApiResponse.ok(null);
}

@GetMapping("/logs")
@RequirePermission("log:view")
public ApiResponse<PageResponse<OperationLog>> logs(@RequestParam(defaultValue = "") String keyword,
Expand Down Expand Up @@ -198,7 +206,7 @@ public ApiResponse<Map<String, Object>> uploadAttachment(@PathVariable Long id,
@RequestParam("file") MultipartFile file) {
SysUser user = authService.requireUser();
Contract contract = contractService.getContract(id);
contractService.ensureCanViewContract(contract.getId(), user);
contractService.ensureCanModifyContract(contract.getId(), user);
Attachment attachment = saveAttachment(contract, file, user);
return ApiResponse.ok("上传成功", Map.of("id", attachment.getId(), "originalName", attachment.getOriginalName()));
}
Expand All @@ -225,7 +233,7 @@ public ResponseEntity<Resource> downloadAttachment(@PathVariable Long id) {
SysUser user = authService.requireUser();
Attachment attachment = attachmentRepository.findById(id)
.orElseThrow(() -> com.contractsys.common.ApiException.notFound("附件不存在"));
contractService.ensureCanViewContract(attachment.getContract().getId(), user);
contractService.ensureCanModifyContract(attachment.getContract().getId(), user);
Path filePath = fileStorageService.resolve(attachment.getStoredName());
if (!Files.exists(filePath)) {
throw com.contractsys.common.ApiException.notFound("附件文件不存在");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ public ContractView create(ContractCreateRequest request, SysUser operator) {
@Transactional
public ContractDetailView assign(Long id, AssignRequest request, SysUser operator) {
Contract contract = getContract(id);
ensureMutableContract(contract);
requireStatus(contract, ContractStatus.DRAFT);
finishTask(id, operator, TaskType.ASSIGN, TaskStatus.DONE, "已完成分配");
completeRemainingPendingTasks(id, TaskType.ASSIGN, "其他分配待办已关闭");
Expand All @@ -124,6 +125,7 @@ public List<TaskView> myTasks(SysUser user) {
@Transactional
public ContractDetailView countersign(Long id, OpinionRequest request, SysUser operator) {
Contract contract = getContract(id);
ensureMutableContract(contract);
requireStatus(contract, ContractStatus.ASSIGNED);
finishTask(id, operator, TaskType.COUNTERSIGN, TaskStatus.DONE, request.opinion());
if (!taskRepository.existsByContractIdAndTaskTypeAndTaskStatus(id, TaskType.COUNTERSIGN, TaskStatus.PENDING)) {
Expand All @@ -136,6 +138,7 @@ public ContractDetailView countersign(Long id, OpinionRequest request, SysUser o
@Transactional
public ContractDetailView finalizeContract(Long id, FinalizeRequest request, SysUser operator) {
Contract contract = getContract(id);
ensureMutableContract(contract);
requireStatus(contract, ContractStatus.COUNTERSIGNED, ContractStatus.REJECTED);
if (!contract.getDrafter().getId().equals(operator.getId())) {
throw ApiException.forbidden("只有起草人可以定稿");
Expand All @@ -149,6 +152,7 @@ public ContractDetailView finalizeContract(Long id, FinalizeRequest request, Sys
@Transactional
public ContractDetailView approve(Long id, ApproveRequest request, SysUser operator) {
Contract contract = getContract(id);
ensureMutableContract(contract);
requireStatus(contract, ContractStatus.FINALIZED);
TaskStatus taskStatus = request.result() == ApproveResult.APPROVED ? TaskStatus.DONE : TaskStatus.REJECTED;
finishTask(id, operator, TaskType.APPROVAL, taskStatus, request.opinion());
Expand All @@ -163,6 +167,7 @@ public ContractDetailView approve(Long id, ApproveRequest request, SysUser opera
@Transactional
public ContractDetailView sign(Long id, SignRequest request, SysUser operator) {
Contract contract = getContract(id);
ensureMutableContract(contract);
requireStatus(contract, ContractStatus.APPROVED);
finishTask(id, operator, TaskType.SIGN, TaskStatus.DONE, request.signInfo());
contract.setSignedDate(request.signedDate());
Expand All @@ -176,6 +181,7 @@ public ContractDetailView sign(Long id, SignRequest request, SysUser operator) {
@Transactional
public ContractView update(Long id, ContractCreateRequest request, SysUser operator) {
Contract contract = getContract(id);
ensureMutableContract(contract);
requireStatus(contract, ContractStatus.DRAFT, ContractStatus.COUNTERSIGNED, ContractStatus.REJECTED);
if (!contract.getDrafter().getId().equals(operator.getId())) {
throw ApiException.forbidden("只有起草人可以修改合同");
Expand Down Expand Up @@ -206,6 +212,16 @@ public void delete(Long id, SysUser operator) {
recordState(contract, contract.getStatus(), contract.getStatus(), operator, "删除合同");
}

@Transactional
public void cancel(Long id, SysUser operator) {
Contract contract = getContract(id);
if (contract.getStatus() == ContractStatus.SIGNED || contract.getStatus() == ContractStatus.CANCELLED) {
throw ApiException.conflict("当前合同状态不允许取消");
}
completeRemainingPendingTasks(id, "合同已取消,待办已关闭");
changeStatus(contract, ContractStatus.CANCELLED, operator, "取消合同");
}

public Page<ContractStateHistory> logs(String keyword, int page, int size) {
var pr = PageRequests.of(page, size);
if (keyword == null || keyword.isEmpty()) {
Expand All @@ -217,15 +233,13 @@ public Page<ContractStateHistory> logs(String keyword, int page, int size) {
@Transactional
public ContractDetailView resubmit(Long id, SysUser operator) {
Contract contract = getContract(id);
ensureMutableContract(contract);
requireStatus(contract, ContractStatus.REJECTED);
// Reset rejected approval tasks back to PENDING
List<ContractTask> approvalTasks = taskRepository.findByContractIdAndTaskType(contract.getId(), TaskType.APPROVAL);
for (ContractTask task : approvalTasks) {
if (task.getTaskStatus() == TaskStatus.REJECTED) {
task.setTaskStatus(TaskStatus.PENDING);
task.setOpinion(null);
task.setOperatedAt(null);
}
task.setTaskStatus(TaskStatus.PENDING);
task.setOpinion(null);
task.setOperatedAt(null);
}
taskRepository.saveAll(approvalTasks);
changeStatus(contract, ContractStatus.FINALIZED, operator, "重新提交审批");
Expand Down Expand Up @@ -324,6 +338,12 @@ public void ensureCanViewContract(Long contractId, SysUser user) {
ensureCanViewContract(getContract(contractId), user);
}

public void ensureCanModifyContract(Long contractId, SysUser user) {
Contract contract = getContract(contractId);
ensureCanViewContract(contract, user);
ensureMutableContract(contract);
}

public boolean canViewContract(Contract contract, SysUser user) {
return hasPermission(user, "contract:query")
|| contract.getDrafter().getId().equals(user.getId())
Expand Down Expand Up @@ -411,6 +431,22 @@ private void completeRemainingPendingTasks(Long contractId, TaskType taskType, S
taskRepository.saveAll(tasks);
}

private void completeRemainingPendingTasks(Long contractId, String opinion) {
List<ContractTask> tasks = taskRepository.findByContractIdAndTaskStatus(contractId, TaskStatus.PENDING);
for (ContractTask task : tasks) {
task.setTaskStatus(TaskStatus.DONE);
task.setOpinion(opinion);
task.setOperatedAt(LocalDateTime.now());
}
taskRepository.saveAll(tasks);
}

private void ensureMutableContract(Contract contract) {
if (contract.getStatus() == ContractStatus.CANCELLED) {
throw ApiException.conflict("已取消合同不能继续操作");
}
}

private void requireStatus(Contract contract, ContractStatus... statuses) {
for (ContractStatus status : statuses) {
if (contract.getStatus() == status) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,112 @@

import com.contractsys.auth.AuthService;
import com.contractsys.auth.RequirePermission;
import com.contractsys.common.ApiException;
import com.contractsys.common.ApiResponse;
import com.contractsys.log.OperationLogService;
import com.contractsys.user.dto.PermissionRequest;
import com.contractsys.user.dto.PermissionView;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Set;

@RestController
@RequestMapping("/api/v1/permissions")
@RequirePermission({"permission:manage", "role:manage"})
public class PermissionController {
private static final Set<String> CORE_PERMISSION_CODES = Set.of(
"contract:create",
"contract:update",
"contract:delete",
"contract:view",
"contract:query",
"contract:assign",
"contract:countersign",
"contract:approve",
"contract:sign",
"customer:manage",
"user:manage",
"role:manage",
"permission:manage",
"log:view"
);

private final PermissionRepository permissionRepository;
private final RoleRepository roleRepository;
private final AuthService authService;
private final OperationLogService operationLogService;

public PermissionController(PermissionRepository permissionRepository, AuthService authService) {
public PermissionController(PermissionRepository permissionRepository, RoleRepository roleRepository,
AuthService authService, OperationLogService operationLogService) {
this.permissionRepository = permissionRepository;
this.roleRepository = roleRepository;
this.authService = authService;
this.operationLogService = operationLogService;
}

@GetMapping
public ApiResponse<List<PermissionView>> list() {
authService.requireUser();
return ApiResponse.ok(permissionRepository.findAll().stream().map(PermissionView::from).toList());
}

@PostMapping
@RequirePermission("permission:manage")
public ApiResponse<PermissionView> create(@Valid @RequestBody PermissionRequest request) {
SysUser operator = authService.requireUser();
if (permissionRepository.existsByPermissionCode(request.permissionCode())) {
throw ApiException.conflict("权限编码已存在");
}
SysPermission permission = new SysPermission();
permission.setPermissionCode(request.permissionCode());
applyRequest(permission, request);
permissionRepository.save(permission);
operationLogService.record(operator, "SYSTEM", "新增权限", "PERMISSION", permission.getId(),
permission.getPermissionCode());
return ApiResponse.ok("创建成功", PermissionView.from(permission));
}

@PutMapping("/{id}")
@RequirePermission("permission:manage")
public ApiResponse<PermissionView> update(@PathVariable Long id,
@Valid @RequestBody PermissionRequest request) {
SysUser operator = authService.requireUser();
SysPermission permission = permissionRepository.findById(id)
.orElseThrow(() -> ApiException.notFound("权限不存在"));
if (!permission.getPermissionCode().equals(request.permissionCode())) {
throw ApiException.conflict("权限编码创建后不可修改");
}
applyRequest(permission, request);
permissionRepository.save(permission);
operationLogService.record(operator, "SYSTEM", "修改权限", "PERMISSION", permission.getId(),
permission.getPermissionCode());
return ApiResponse.ok("更新成功", PermissionView.from(permission));
}

@DeleteMapping("/{id}")
@RequirePermission("permission:manage")
public ApiResponse<Void> delete(@PathVariable Long id) {
SysUser operator = authService.requireUser();
SysPermission permission = permissionRepository.findById(id)
.orElseThrow(() -> ApiException.notFound("权限不存在"));
if (CORE_PERMISSION_CODES.contains(permission.getPermissionCode())) {
throw ApiException.conflict("系统核心权限不能删除");
}
if (roleRepository.existsByPermissions_Id(id)) {
throw ApiException.conflict("该权限已分配给角色,不能删除");
}
permissionRepository.delete(permission);
operationLogService.record(operator, "SYSTEM", "删除权限", "PERMISSION", id,
permission.getPermissionCode());
return ApiResponse.ok(null);
}

private void applyRequest(SysPermission permission, PermissionRequest request) {
permission.setPermissionName(request.permissionName());
permission.setModule(request.module());
permission.setUrl(request.url());
permission.setDescription(request.description());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
public interface RoleRepository extends JpaRepository<SysRole, Long> {
Optional<SysRole> findByRoleCode(String roleCode);
boolean existsByRoleCode(String roleCode);
boolean existsByPermissions_Id(Long permissionId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ public ApiResponse<UserView> update(@PathVariable Long id,
if (request.phone() != null) user.setPhone(request.phone());
if (request.email() != null) user.setEmail(request.email());
if (request.password() != null && !request.password().isBlank()) {
if (request.password().length() < 6) {
throw ApiException.badRequest("密码长度不能少于6位");
}
user.setPasswordHash(passwordEncoder.encode(request.password()));
}
userRepository.save(user);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.contractsys.user.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public record PermissionRequest(
@NotBlank @Size(max = 80) String permissionCode,
@NotBlank @Size(max = 40) String permissionName,
@NotBlank @Size(max = 40) String module,
@Size(max = 200) String url,
@Size(max = 100) String description
) {
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package com.contractsys.user.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

import java.util.List;

public record UserCreateRequest(
@NotBlank String username,
@NotBlank String password,
@NotBlank @Size(min = 6, max = 100) String password,
String displayName,
String phone,
String email,
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/layouts/MainLayout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ref, computed, watch, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import {
ClipboardList, UsersRound, Handshake, LogOut,
LayoutDashboard, Settings, ShieldCheck, UserCog, ScrollText, ChevronDown, Search
LayoutDashboard, Settings, ShieldCheck, UserCog, ScrollText, ChevronDown, Search, KeyRound
} from 'lucide-vue-next'
import { useAuthStore } from '../stores/auth'
import { api } from '../api'
Expand All @@ -23,6 +23,7 @@ const menuItems = [
const sysItems = [
{ path: '/system/users', label: '用户管理', icon: UserCog, permission: 'user:manage' },
{ path: '/system/roles', label: '角色管理', icon: ShieldCheck, permission: 'role:manage' },
{ path: '/system/permissions', label: '权限管理', icon: KeyRound, permission: 'permission:manage' },
{ path: '/system/logs', label: '操作日志', icon: ScrollText, permission: 'log:view' },
]

Expand Down
2 changes: 2 additions & 0 deletions frontend/src/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import CustomerListView from './views/CustomerListView.vue'
import MyTasksView from './views/MyTasksView.vue'
import UserManagementView from './views/system/UserManagementView.vue'
import RoleManagementView from './views/system/RoleManagementView.vue'
import PermissionManagementView from './views/system/PermissionManagementView.vue'
import LogView from './views/system/LogView.vue'

function hasAccess(required, permissions) {
Expand Down Expand Up @@ -39,6 +40,7 @@ const router = createRouter({
{ path: 'tasks', component: MyTasksView, meta: { title: '我的待办', permission: ['contract:assign', 'contract:countersign', 'contract:approve', 'contract:sign', 'contract:update'] } },
{ path: 'system/users', component: UserManagementView, meta: { title: '用户管理', permission: 'user:manage' } },
{ path: 'system/roles', component: RoleManagementView, meta: { title: '角色管理', permission: 'role:manage' } },
{ path: 'system/permissions', component: PermissionManagementView, meta: { title: '权限管理', permission: 'permission:manage' } },
{ path: 'system/logs', component: LogView, meta: { title: '操作日志', permission: 'log:view' } },
]
}
Expand Down
Loading