You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Angular 5应用用户交互审计与表单回填展示方案咨询

嗨,针对你这个用户交互审计+表单状态还原的需求,结合你提到的Angular 5 + Spring Boot技术栈,我整理了一套从后端捕获请求到前端还原表单的完整可行方案,具体如下:

后端实现(Spring Boot + Servlet Filter)

1. 自定义请求包装类解决请求体只读问题

因为原生HttpServletRequest的输入流只能读取一次,Filter读取后Controller就无法获取请求体了,所以需要先实现一个请求包装类缓存请求体:

public class CachedBodyHttpServletRequestWrapper extends HttpServletRequestWrapper {
    private byte[] cachedBody;

    public CachedBodyHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        this.cachedBody = StreamUtils.copyToByteArray(request.getInputStream());
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        return new CachedBodyServletInputStream(this.cachedBody);
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(new ByteArrayInputStream(this.cachedBody)));
    }

    public byte[] getCachedBody() {
        return this.cachedBody;
    }

    static class CachedBodyServletInputStream extends ServletInputStream {
        private final ByteArrayInputStream inputStream;

        public CachedBodyServletInputStream(byte[] cachedBody) {
            this.inputStream = new ByteArrayInputStream(cachedBody);
        }

        @Override
        public boolean isFinished() {
            return inputStream.available() == 0;
        }

        @Override
        public boolean isReady() {
            return true;
        }

        @Override
        public void setReadListener(ReadListener readListener) {
            throw new UnsupportedOperationException();
        }

        @Override
        public int read() throws IOException {
            return inputStream.read();
        }
    }
}

2. 实现审计Filter捕获JSON请求

在Filter中筛选需要审计的请求(POST/PUT类型、JSON格式),提取用户信息、请求体等数据,异步存入MongoDB:

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class AuditFilter implements Filter {

    private final AuditRecordRepository auditRepo;

    public AuditFilter(AuditRecordRepository auditRepo) {
        this.auditRepo = auditRepo;
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String method = httpRequest.getMethod();
        String contentType = httpRequest.getContentType();

        // 只处理POST/PUT的JSON请求
        if (("POST".equals(method) || "PUT".equals(method)) 
            && contentType != null && contentType.contains("application/json")) {
            CachedBodyHttpServletRequestWrapper cachedRequest = new CachedBodyHttpServletRequestWrapper(httpRequest);
            String requestBody = new String(cachedRequest.getCachedBody(), StandardCharsets.UTF_8);

            // 获取当前登录用户(基于Spring Security)
            Authentication auth = SecurityContextHolder.getContext().getAuthentication();
            String userId = auth != null ? auth.getName() : "anonymous";

            // 构建审计记录
            AuditRecord record = new AuditRecord();
            record.setUserId(userId);
            record.setRequestUrl(httpRequest.getRequestURI());
            record.setRequestMethod(method);
            record.setRequestBody(requestBody);
            record.setCreateTime(LocalDateTime.now());
            // 从请求头获取表单标识,方便前端还原时识别表单类型
            record.setFormKey(httpRequest.getHeader("X-Form-Key"));

            // 异步保存,避免阻塞业务请求
            saveAuditRecordAsync(record);

            chain.doFilter(cachedRequest, response);
        } else {
            chain.doFilter(request, response);
        }
    }

    @Async
    protected void saveAuditRecordAsync(AuditRecord record) {
        auditRepo.save(record);
    }

    @Override
    public void init(FilterConfig filterConfig) {}

    @Override
    public void destroy() {}
}

3. 审计实体与查询接口

定义MongoDB实体类,并实现管理员专属的查询接口:

@Document(collection = "audit_records")
public class AuditRecord {
    @Id
    private String id;
    private String userId;
    private String requestUrl;
    private String requestMethod;
    private String requestBody;
    private LocalDateTime createTime;
    private String formKey; // 表单唯一标识

    // getter、setter省略
}

// 管理员查询接口示例
@RestController
@RequestMapping("/api/admin/audit")
@PreAuthorize("hasRole('ADMIN')")
public class AdminAuditController {
    private final AuditRecordRepository auditRepo;

    public AdminAuditController(AuditRecordRepository auditRepo) {
        this.auditRepo = auditRepo;
    }

    @GetMapping("/records")
    public Page<AuditRecord> getAuditRecords(
            @RequestParam(required = false) String userId,
            @RequestParam(required = false) LocalDateTime startDate,
            @PageableDefault Pageable pageable) {
        // 构建查询条件
        Criteria criteria = new Criteria();
        if (userId != null) criteria.and("userId").is(userId);
        if (startDate != null) criteria.and("createTime").gte(startDate);
        return auditRepo.findAll(criteria, pageable);
    }

    @GetMapping("/records/{id}")
    public AuditRecord getAuditRecord(@PathVariable String id) {
        return auditRepo.findById(id).orElseThrow(() -> new ResourceNotFoundException("Audit record not found"));
    }
}
前端实现(Angular 5)

1. 表单提交时携带标识

使用Reactive Forms构建表单,提交时通过请求头传递表单唯一标识,方便后端记录:

@Component({
  selector: 'app-user-profile-form',
  templateUrl: './user-profile-form.component.html'
})
export class UserProfileFormComponent implements OnInit {
  userForm: FormGroup;
  private readonly FORM_KEY = 'user-profile-form';

  constructor(private fb: FormBuilder, private http: HttpClient) {}

  ngOnInit(): void {
    this.initForm();
  }

  initForm(): void {
    this.userForm = this.fb.group({
      username: ['', Validators.required],
      email: ['', [Validators.required, Validators.email]],
      phone: [''],
      isActive: [true]
    });
  }

  onSubmit(): void {
    if (this.userForm.valid) {
      const headers = new HttpHeaders().set('X-Form-Key', this.FORM_KEY);
      this.http.post('/api/user/profile', this.userForm.value, { headers }).subscribe(
        () => alert('提交成功'),
        err => console.error('提交失败', err)
      );
    }
  }
}

2. 管理员审计页面与表单还原

实现审计记录列表页,点击记录后根据formKey动态加载对应表单组件并还原状态:

// 定义表单还原接口
export interface FormRestorable {
  restoreForm(data: any): void;
}

// 审计记录详情页组件
@Component({
  selector: 'app-audit-detail',
  template: `<div #formContainer></div>`
})
export class AuditDetailComponent implements OnInit, OnDestroy {
  @ViewChild('formContainer', { read: ViewContainerRef }) formContainer!: ViewContainerRef;
  private componentRef: ComponentRef<any> | null = null;

  constructor(
    private route: ActivatedRoute,
    private auditService: AuditService,
    private cfr: ComponentFactoryResolver
  ) {}

  ngOnInit(): void {
    const recordId = this.route.snapshot.paramMap.get('id');
    if (recordId) {
      this.auditService.getAuditRecord(recordId).subscribe(record => {
        this.loadAndRestoreForm(record.formKey, record.requestBody);
      });
    }
  }

  loadAndRestoreForm(formKey: string, requestBody: string): void {
    // 表单组件映射表
    const componentMap: { [key: string]: Type<any> } = {
      'user-profile-form': UserProfileFormComponent,
      'order-submit-form': OrderSubmitFormComponent
    };

    const componentType = componentMap[formKey];
    if (componentType) {
      const factory = this.cfr.resolveComponentFactory(componentType);
      this.formContainer.clear();
      this.componentRef = this.formContainer.createComponent(factory);
      // 调用还原方法
      const restorable = this.componentRef.instance as FormRestorable;
      restorable.restoreForm(JSON.parse(requestBody));
    } else {
      // 处理未知表单
      this.formContainer.createComponent(UnknownFormComponent);
    }
  }

  ngOnDestroy(): void {
    this.componentRef?.destroy();
  }
}

// 在表单组件中实现还原方法
export class UserProfileFormComponent implements OnInit, FormRestorable {
  // ... 原有代码

  restoreForm(data: any): void {
    this.initForm();
    // 还原表单值
    this.userForm.patchValue(data);
    // 如果需要还原控件状态(如禁用),可以在后端额外存储元数据
  }
}
关键注意事项
  • 请求体内存控制:如果表单数据过大,可在请求包装类中设置最大缓存大小,避免OOM;
  • 用户身份准确性:确保Filter中获取的用户是真实提交表单的用户,依赖Spring Security的Authentication上下文;
  • 表单兼容性:若后续表单结构变更,还原旧记录时需处理字段不匹配的情况,比如忽略不存在的字段;
  • 异步保存可靠性:配置合理的异步线程池,同时添加失败重试或错误日志记录机制。

内容的提问来源于stack exchange,提问作者Amir Pashazadeh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.25 06:35:49