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

如何用.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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 00:13:17