如何用.NET Web API和Angular从单表单保存数据至两张表
题库表单数据分表保存实现方案(.NET Web API + Angular)
一、.NET Web API 后端实现
1. 定义实体模型
对应数据库表结构,配置实体关联关系:
// 分类表实体 public class Category { public int CategoryId { get; set; } public string Name { get; set; } } // 题目类型表实体 public class QuestionType { public int TypeId { get; set; } public string Name { get; set; } } // 题目表实体 public class Question { public int QuestionId { get; set; } public int CategoryId { get; set; } public int TypeId { get; set; } public decimal Weightage { get; set; } public string Description { get; set; } // 导航属性 public Category Category { get; set; } public QuestionType QuestionType { get; set; } public ICollection<QuestionAnswer> QuestionAnswers { get; set; } = new List<QuestionAnswer>(); } // 题目答案表实体 public class QuestionAnswer { public int QuestionAnswerId { get; set; } public int QuestionId { get; set; } public int TypeId { get; set; } // 答案类型(如正确/错误选项) public string Options { get; set; } public int Points { get; set; } // 导航属性 public Question Question { get; set; } }
2. 创建DTO(数据传输对象)
用于接收前端提交的组合数据,同时做数据验证:
public class CreateQuestionDto { [Required] public int CategoryId { get; set; } [Required] public int TypeId { get; set; } [Required] public decimal Weightage { get; set; } [Required] public string Description { get; set; } [Required] [MinLength(1)] public List<CreateQuestionAnswerDto> Answers { get; set; } } public class CreateQuestionAnswerDto { [Required] public int TypeId { get; set; } [Required] public string Options { get; set; } [Required] public int Points { get; set; } }
3. 配置DbContext
定义表关系与约束,确保事务支持:
public class AppDbContext : DbContext { public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { } public DbSet<Category> Categories { get; set; } public DbSet<QuestionType> QuestionTypes { get; set; } public DbSet<Question> Questions { get; set; } public DbSet<QuestionAnswer> QuestionAnswers { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { // 题目与答案一对多关联,删除题目时级联删除答案 modelBuilder.Entity<Question>() .HasMany(q => q.QuestionAnswers) .WithOne(a => a.Question) .HasForeignKey(a => a.QuestionId) .OnDelete(DeleteBehavior.Cascade); // 题目与分类、类型的多对一关联 modelBuilder.Entity<Question>() .HasOne(q => q.Category) .WithMany() .HasForeignKey(q => q.CategoryId); modelBuilder.Entity<Question>() .HasOne(q => q.QuestionType) .WithMany() .HasForeignKey(q => q.TypeId); } }
4. 编写API控制器
实现POST接口,用事务保证数据一致性:
[ApiController] [Route("api/[controller]")] public class QuestionsController : ControllerBase { private readonly AppDbContext _context; public QuestionsController(AppDbContext context) { _context = context; } [HttpPost] public async Task<IActionResult> CreateQuestion([FromBody] CreateQuestionDto dto) { if (!ModelState.IsValid) return BadRequest(ModelState); // 验证关联的分类、类型是否存在 var categoryExists = await _context.Categories.AnyAsync(c => c.CategoryId == dto.CategoryId); var typeExists = await _context.QuestionTypes.AnyAsync(t => t.TypeId == dto.TypeId); if (!categoryExists) return BadRequest("指定分类不存在"); if (!typeExists) return BadRequest("指定题目类型不存在"); // 开启事务 using var transaction = await _context.Database.BeginTransactionAsync(); try { // 保存题目,获取自动生成的QuestionId var question = new Question { CategoryId = dto.CategoryId, TypeId = dto.TypeId, Weightage = dto.Weightage, Description = dto.Description }; _context.Questions.Add(question); await _context.SaveChangesAsync(); // 保存关联的答案 foreach (var answerDto in dto.Answers) { var answer = new QuestionAnswer { QuestionId = question.QuestionId, TypeId = answerDto.TypeId, Options = answerDto.Options, Points = answerDto.Points }; _context.QuestionAnswers.Add(answer); } await _context.SaveChangesAsync(); await transaction.CommitAsync(); return CreatedAtAction(nameof(GetQuestion), new { id = question.QuestionId }, question); } catch (Exception ex) { await transaction.RollbackAsync(); return StatusCode(500, $"保存失败:{ex.Message}"); } } [HttpGet("{id}")] public async Task<IActionResult> GetQuestion(int id) { var question = await _context.Questions.Include(q => q.QuestionAnswers) .FirstOrDefaultAsync(q => q.QuestionId == id); return question == null ? NotFound() : Ok(question); } }
5. 基础配置
在Program.cs中配置CORS、数据库连接与验证:
var builder = WebApplication.CreateBuilder(args); builder.Services.AddDbContext<AppDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); builder.Services.AddControllers() .AddJsonOptions(opt => opt.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles); // 允许Angular跨域访问 builder.Services.AddCors(opt => { opt.AddPolicy("AllowAngular", policy => policy .WithOrigins("http://localhost:4200") .AllowAnyHeader() .AllowAnyMethod()); }); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseCors("AllowAngular"); app.UseAuthorization(); app.MapControllers(); app.Run();
二、Angular 前端实现
1. 定义TypeScript接口
对应后端DTO与返回实体:
// models/question.model.ts export interface CreateQuestionDto { categoryId: number; typeId: number; weightage: number; description: string; answers: CreateQuestionAnswerDto[]; } export interface CreateQuestionAnswerDto { typeId: number; options: string; points: number; } export interface Question { questionId: number; categoryId: number; typeId: number; weightage: number; description: string; questionAnswers: QuestionAnswer[]; } export interface QuestionAnswer { questionAnswerId: number; questionId: number; typeId: number; options: string; points: number; }
2. 题目表单组件
创建动态表单支持添加多个答案选项:
// question-form.component.ts import { Component } from '@angular/core'; import { FormBuilder, FormGroup, FormArray, Validators } from '@angular/forms'; import { QuestionService } from '../services/question.service'; import { Router } from '@angular/router'; @Component({ selector: 'app-question-form', templateUrl: './question-form.component.html', styleUrls: ['./question-form.component.css'] }) export class QuestionFormComponent { questionForm: FormGroup; categories: { id: number, name: string }[] = []; questionTypes: { id: number, name: string }[] = []; answerTypes: { id: number, name: string }[] = []; get answers(): FormArray { return this.questionForm.get('answers') as FormArray; } constructor( private fb: FormBuilder, private questionService: QuestionService, private router: Router ) { // 初始化表单 this.questionForm = this.fb.group({ categoryId: ['', Validators.required], typeId: ['', Validators.required], weightage: ['', [Validators.required, Validators.min(0)]], description: ['', Validators.required], answers: this.fb.array([this.createAnswerGroup()], Validators.minLength(1)) }); // 模拟数据,实际需从后端接口获取 this.categories = [{ id: 1, name: '前端开发' }, { id: 2, name: '后端开发' }]; this.questionTypes = [{ id: 1, name: '单选题' }, { id: 2, name: '多选题' }]; this.answerTypes = [{ id: 1, name: '正确选项' }, { id: 2, name: '错误选项' }]; } createAnswerGroup(): FormGroup { return this.fb.group({ typeId: ['', Validators.required], options: ['', Validators.required], points: ['', [Validators.required, Validators.min(0)]] }); } addAnswer(): void { this.answers.push(this.createAnswerGroup()); } removeAnswer(index: number): void { if (this.answers.length > 1) this.answers.removeAt(index); } onSubmit(): void { if (this.questionForm.invalid) return; this.questionService.createQuestion(this.questionForm.value).subscribe({ next: () => { alert('题目保存成功'); this.router.navigate(['/questions']); }, error: err => alert(`保存失败:${err.error || '未知错误'}`) }); } }
组件模板:
<!-- question-form.component.html --> <div class="container mt-4"> <h2>新增题目</h2> <form [formGroup]="questionForm" (ngSubmit)="onSubmit()"> <!-- 题目分类 --> <div class="form-group mb-3"> <label>题目分类</label> <select formControlName="categoryId" class="form-control"> <option value="">请选择分类</option> <option *ngFor="let cate of categories" [value]="cate.id">{{ cate.name }}</option> </select> <div *ngIf="questionForm.get('categoryId')?.invalid && questionForm.get('categoryId')?.touched" class="text-danger"> 请选择分类 </div> </div> <!-- 题目类型 --> <div class="form-group mb-3"> <label>题目类型</label> <select formControlName="typeId" class="form-control"> <option value="">请选择类型</option> <option *ngFor="let type of questionTypes" [value]="type.id">{{ type.name }}</option> </select> <div *ngIf="questionForm.get('typeId')?.invalid && questionForm.get('typeId')?.touched" class="text-danger"> 请选择类型 </div> </div> <!-- 题目权重与描述 --> <div class="row mb-3"> <div class="col-md-6"> <label>题目权重</label> <input type="number" formControlName="weightage" class="form-control" step="0.1"> <div *ngIf="questionForm.get('weightage')?.invalid && questionForm.get('weightage')?.touched" class="text-danger"> <span *ngIf="questionForm.get('weightage')?.errors?.['required']">请输入权重</span> <span *ngIf="questionForm.get('weightage')?.errors?.['min']">权重不能小于0</span> </div> </div> </div> <div class="form-group mb-3"> <label>题目描述</label> <textarea formControlName="description" class="form-control" rows="3"></textarea> <div *ngIf="questionForm.get('description')?.invalid && questionForm.get('description')?.touched" class="text-danger"> 请输入题目描述 </div> </div> <!-- 答案选项 --> <div formArrayName="answers" class="mb-4"> <h4>答案选项</h4> <div *ngFor="let answer of answers.controls; let i = index" [formGroupName]="i" class="border p-3 mb-2"> <div class="row"> <div class="col-md-4"> <label>答案类型</label> <select formControlName="typeId" class="form-control"> <option value="">请选择类型</option> <option *ngFor="let type of answerTypes" [value]="type.id">{{ type.name }}</option> </select> <div *ngIf="answer.get('typeId')?.invalid && answer.get('typeId')?.touched" class="text-danger"> 请选择答案类型 </div> </div> <div class="col-md-4"> <label>选项内容</label> <input type="text" formControlName="options" class="form-control"> <div *ngIf="answer.get('options')?.invalid && answer.get('options')?.touched" class="text-danger"> 请输入选项内容 </div> </div> <div class="col-md-3"> <label>分值</label> <input type="number" formControlName="points" class="form-control"> <div *ngIf="answer.get('points')?.invalid && answer.get('points')?.touched" class="text-danger"> <span *ngIf="answer.get('points')?.errors?.['required']">请输入分值</span> <span *ngIf="answer.get('points')?.errors?.['min']">分值不能小于0</span> </div> </div> <div class="col-md-1 align-self-end"> <button type="button" class="btn btn-danger" (click)="removeAnswer(i)" [disabled]="answers.length <=1">删除</button> </div> </div> </div> <button type="button" class="btn btn-secondary" (click)="addAnswer()">添加选项</button> </div> <button type="submit" class="btn btn-primary" [disabled]="questionForm.invalid">保存题目</button> </form> </div>
3. 题目服务
封装API调用:
// question.service.ts import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; import { CreateQuestionDto, Question } from '../models/question.model'; @Injectable({ providedIn: 'root' }) export class QuestionService { private apiUrl = 'https://localhost:7045/api/questions'; // 替换为你的API地址 constructor(private http: HttpClient) { } createQuestion(data: CreateQuestionDto): Observable<Question> { return this.http.post<Question>(this.apiUrl, data); } }
4. 模块配置
在AppModule中导入必要模块:
// app.module.ts import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { ReactiveFormsModule } from '@angular/forms'; import { HttpClientModule } from '@angular/common/http'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { QuestionFormComponent } from './question-form/question-form.component'; @NgModule({ declarations: [AppComponent, QuestionFormComponent], imports: [BrowserModule, AppRoutingModule, ReactiveFormsModule, HttpClientModule], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
内容的提问来源于stack exchange,提问作者Sugaa Krish
相关产品推荐
相关产品推荐

