新增农户功能升级:身份证OCR/OSS上传、头像采集、同名村检测、Region热度排序及表单体验优化
- Region 表新增热度列,热门省份优先显示 - 区县下拉按地级市分组、已录区县优先、支持全省搜索 - 组选择内置一组至二十组 - 银行卡号独立行+粗体预览,开户行独立行 - 新增农户类型:农户/个体户/合作社/经营集体/公司 - 身份证正反面支持高拍仪/摄像头OCR与手机扫码上传,附件存本地/OSS - 新增农户头像采集组件(摄像头/本地上传),预留人脸识别收购扩展 - 同名村弹窗提醒(防选错乡镇) - 编辑窗体禁止遮罩/Esc误关,整体加宽适配高频操作 - 修复二维码局域网手机无法访问(监听0.0.0.0、局域网IP识别、Vite代理兜底)
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
using AgriculturalPlatform.Api.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace AgriculturalPlatform.Api.Data;
|
||||
|
||||
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
|
||||
{
|
||||
public DbSet<User> Users => Set<User>();
|
||||
public DbSet<Organization> Organizations => Set<Organization>();
|
||||
public DbSet<Farmer> Farmers => Set<Farmer>();
|
||||
public DbSet<Region> Regions => Set<Region>();
|
||||
public DbSet<Product> Products => Set<Product>();
|
||||
public DbSet<PurchaseOrder> PurchaseOrders => Set<PurchaseOrder>();
|
||||
public DbSet<PaymentRecord> PaymentRecords => Set<PaymentRecord>();
|
||||
public DbSet<Invoice> Invoices => Set<Invoice>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
// 所有枚举属性以字符串形式存储,便于阅读
|
||||
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
|
||||
{
|
||||
foreach (var property in entityType.GetProperties())
|
||||
{
|
||||
if (property.ClrType.IsEnum)
|
||||
{
|
||||
var converterType = typeof(EnumToStringConverter<>).MakeGenericType(property.ClrType);
|
||||
var converter = (ValueConverter)Activator.CreateInstance(converterType)!;
|
||||
property.SetValueConverter(converter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 用户
|
||||
modelBuilder.Entity<User>()
|
||||
.Property(u => u.Username).HasMaxLength(50);
|
||||
modelBuilder.Entity<User>()
|
||||
.HasIndex(u => u.Username).IsUnique();
|
||||
modelBuilder.Entity<User>()
|
||||
.HasOne(u => u.Org).WithMany().HasForeignKey(u => u.OrgId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
// 组织
|
||||
modelBuilder.Entity<Organization>()
|
||||
.Property(o => o.Type).HasMaxLength(32);
|
||||
modelBuilder.Entity<Organization>()
|
||||
.Property(o => o.Name).HasMaxLength(200);
|
||||
modelBuilder.Entity<Organization>()
|
||||
.HasIndex(o => new { o.Type, o.Name });
|
||||
modelBuilder.Entity<Organization>()
|
||||
.HasOne(o => o.Parent).WithMany().HasForeignKey(o => o.ParentId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
// 农户:身份证唯一
|
||||
modelBuilder.Entity<Farmer>()
|
||||
.Property(f => f.IdCard).HasMaxLength(18);
|
||||
modelBuilder.Entity<Farmer>()
|
||||
.HasIndex(f => f.IdCard).IsUnique();
|
||||
modelBuilder.Entity<Farmer>()
|
||||
.Property(f => f.Phone).HasMaxLength(20);
|
||||
modelBuilder.Entity<Farmer>()
|
||||
.HasIndex(f => f.Phone);
|
||||
modelBuilder.Entity<Farmer>()
|
||||
.Property(f => f.FarmerType).HasMaxLength(20);
|
||||
modelBuilder.Entity<Farmer>()
|
||||
.Property(f => f.IdCardFrontUrl).HasMaxLength(500);
|
||||
modelBuilder.Entity<Farmer>()
|
||||
.Property(f => f.IdCardBackUrl).HasMaxLength(500);
|
||||
modelBuilder.Entity<Farmer>()
|
||||
.Property(f => f.Province).HasMaxLength(50);
|
||||
modelBuilder.Entity<Farmer>()
|
||||
.Property(f => f.County).HasMaxLength(50);
|
||||
modelBuilder.Entity<Farmer>()
|
||||
.Property(f => f.Township).HasMaxLength(50);
|
||||
modelBuilder.Entity<Farmer>()
|
||||
.Property(f => f.Village).HasMaxLength(100);
|
||||
modelBuilder.Entity<Farmer>()
|
||||
.Property(f => f.GroupName).HasMaxLength(50);
|
||||
modelBuilder.Entity<Farmer>()
|
||||
.HasIndex(f => new { f.Province, f.County, f.Township });
|
||||
|
||||
// 行政区划(AreaCity 全国数据,表由导入脚本/启动建表创建)
|
||||
modelBuilder.Entity<Region>()
|
||||
.HasKey(r => r.Id);
|
||||
modelBuilder.Entity<Region>()
|
||||
.Property(r => r.Name).HasMaxLength(100);
|
||||
modelBuilder.Entity<Region>()
|
||||
.Property(r => r.ExtName).HasMaxLength(100);
|
||||
modelBuilder.Entity<Region>()
|
||||
.HasIndex(r => r.Pid);
|
||||
modelBuilder.Entity<Region>()
|
||||
.HasIndex(r => r.Deep);
|
||||
|
||||
// 品种
|
||||
modelBuilder.Entity<Product>()
|
||||
.Property(p => p.Name).HasMaxLength(100);
|
||||
modelBuilder.Entity<Product>()
|
||||
.HasIndex(p => p.Name).IsUnique();
|
||||
|
||||
// 收购单
|
||||
modelBuilder.Entity<PurchaseOrder>()
|
||||
.Property(p => p.OrderNo).HasMaxLength(32);
|
||||
modelBuilder.Entity<PurchaseOrder>()
|
||||
.HasIndex(p => p.OrderNo).IsUnique();
|
||||
modelBuilder.Entity<PurchaseOrder>()
|
||||
.HasIndex(p => new { p.Status, p.CreatedAt });
|
||||
modelBuilder.Entity<PurchaseOrder>()
|
||||
.HasOne(p => p.Farmer).WithMany().HasForeignKey(p => p.FarmerId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
modelBuilder.Entity<PurchaseOrder>()
|
||||
.HasOne(p => p.Product).WithMany().HasForeignKey(p => p.ProductId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
modelBuilder.Entity<PurchaseOrder>()
|
||||
.HasOne(p => p.PurchaserOrg).WithMany().HasForeignKey(p => p.PurchaserOrgId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
modelBuilder.Entity<PurchaseOrder>()
|
||||
.HasOne(p => p.Operator).WithMany().HasForeignKey(p => p.OperatorId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
// 支付
|
||||
modelBuilder.Entity<PaymentRecord>()
|
||||
.Property(p => p.PayNo).HasMaxLength(32);
|
||||
modelBuilder.Entity<PaymentRecord>()
|
||||
.HasIndex(p => p.PayNo).IsUnique();
|
||||
modelBuilder.Entity<PaymentRecord>()
|
||||
.HasIndex(p => new { p.Status, p.CreatedAt });
|
||||
modelBuilder.Entity<PaymentRecord>()
|
||||
.HasOne(p => p.Farmer).WithMany().HasForeignKey(p => p.FarmerId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
modelBuilder.Entity<PaymentRecord>()
|
||||
.HasOne(p => p.PurchaseOrder).WithMany().HasForeignKey(p => p.PurchaseOrderId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
modelBuilder.Entity<PaymentRecord>()
|
||||
.HasOne(p => p.PurchaserOrg).WithMany().HasForeignKey(p => p.PurchaserOrgId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
modelBuilder.Entity<PaymentRecord>()
|
||||
.HasOne(p => p.Operator).WithMany().HasForeignKey(p => p.OperatorId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
// 发票
|
||||
modelBuilder.Entity<Invoice>()
|
||||
.Property(i => i.InvoiceNo).HasMaxLength(32);
|
||||
modelBuilder.Entity<Invoice>()
|
||||
.HasIndex(i => i.InvoiceNo).IsUnique();
|
||||
modelBuilder.Entity<Invoice>()
|
||||
.Property(i => i.BatchNo).HasMaxLength(32);
|
||||
modelBuilder.Entity<Invoice>()
|
||||
.HasIndex(i => i.BatchNo);
|
||||
modelBuilder.Entity<Invoice>()
|
||||
.HasIndex(i => new { i.Status, i.IssueDate });
|
||||
modelBuilder.Entity<Invoice>()
|
||||
.HasOne(i => i.Farmer).WithMany().HasForeignKey(i => i.FarmerId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
modelBuilder.Entity<Invoice>()
|
||||
.HasOne(i => i.PurchaserOrg).WithMany().HasForeignKey(i => i.PurchaserOrgId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
modelBuilder.Entity<Invoice>()
|
||||
.HasOne(i => i.Product).WithMany().HasForeignKey(i => i.ProductId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
modelBuilder.Entity<Invoice>()
|
||||
.HasOne(i => i.PurchaseOrder).WithMany().HasForeignKey(i => i.PurchaseOrderId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
modelBuilder.Entity<Invoice>()
|
||||
.HasOne(i => i.Operator).WithMany().HasForeignKey(i => i.OperatorId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
// 金额/重量精度
|
||||
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
|
||||
{
|
||||
foreach (var property in entityType.GetProperties())
|
||||
{
|
||||
if (property.ClrType == typeof(decimal) || property.ClrType == typeof(decimal?))
|
||||
{
|
||||
property.SetPrecision(18);
|
||||
property.SetScale(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
using AgriculturalPlatform.Api.Models;
|
||||
|
||||
namespace AgriculturalPlatform.Api.Data;
|
||||
|
||||
/// <summary>初始化数据(仅在空库时执行)</summary>
|
||||
public static class DbSeeder
|
||||
{
|
||||
public static void Seed(AppDbContext db)
|
||||
{
|
||||
if (db.Users.Any()) return;
|
||||
|
||||
var now = DateTime.Now;
|
||||
|
||||
// ---------- 组织 ----------
|
||||
var company = new Organization
|
||||
{
|
||||
Type = OrgType.Company,
|
||||
Name = "绿源农产品收购有限公司",
|
||||
ContactPerson = "王建国",
|
||||
Phone = "13800000001",
|
||||
Address = "河南省郑州市中牟县官渡大道66号",
|
||||
TaxNo = "91410100MA3X8K7Q2B",
|
||||
CreatedAt = now
|
||||
};
|
||||
var stationEast = new Organization
|
||||
{
|
||||
Type = OrgType.Station,
|
||||
Name = "城东收购站",
|
||||
ContactPerson = "李强",
|
||||
Phone = "13800000002",
|
||||
Address = "中牟县官渡镇城东村",
|
||||
CreatedAt = now
|
||||
};
|
||||
var stationWest = new Organization
|
||||
{
|
||||
Type = OrgType.Station,
|
||||
Name = "城西收购站",
|
||||
ContactPerson = "赵敏",
|
||||
Phone = "13800000003",
|
||||
Address = "中牟县韩寺镇城西村",
|
||||
CreatedAt = now
|
||||
};
|
||||
var individual = new Organization
|
||||
{
|
||||
Type = OrgType.Individual,
|
||||
Name = "张伟(个体收购)",
|
||||
ContactPerson = "张伟",
|
||||
Phone = "13800000004",
|
||||
Address = "中牟县刁家乡",
|
||||
TaxNo = "410122197501011234",
|
||||
CreatedAt = now
|
||||
};
|
||||
|
||||
db.Organizations.AddRange(company, stationEast, stationWest, individual);
|
||||
db.SaveChanges();
|
||||
stationEast.ParentId = company.Id;
|
||||
stationWest.ParentId = company.Id;
|
||||
db.SaveChanges();
|
||||
|
||||
// ---------- 用户(默认密码均为 123456) ----------
|
||||
var users = new List<User>
|
||||
{
|
||||
new() { Username = "admin", PasswordHash = BCrypt.Net.BCrypt.HashPassword("123456"), RealName = "系统管理员", Phone = "13800000000", Role = UserRole.SuperAdmin, CreatedAt = now },
|
||||
new() { Username = "company", PasswordHash = BCrypt.Net.BCrypt.HashPassword("123456"), RealName = "王建国", Phone = "13800000001", Role = UserRole.CompanyAdmin, OrgId = company.Id, CreatedAt = now },
|
||||
new() { Username = "station1", PasswordHash = BCrypt.Net.BCrypt.HashPassword("123456"), RealName = "李强", Phone = "13800000002", Role = UserRole.StationStaff, OrgId = stationEast.Id, CreatedAt = now },
|
||||
new() { Username = "station2", PasswordHash = BCrypt.Net.BCrypt.HashPassword("123456"), RealName = "赵敏", Phone = "13800000003", Role = UserRole.StationStaff, OrgId = stationWest.Id, CreatedAt = now },
|
||||
new() { Username = "individual", PasswordHash = BCrypt.Net.BCrypt.HashPassword("123456"), RealName = "张伟", Phone = "13800000004", Role = UserRole.Individual, OrgId = individual.Id, CreatedAt = now }
|
||||
};
|
||||
db.Users.AddRange(users);
|
||||
db.SaveChanges();
|
||||
|
||||
// ---------- 品种 ----------
|
||||
var products = new List<Product>
|
||||
{
|
||||
new() { Name = "小麦", Category = "粮食", Unit = "公斤", Spec = "水分≤13%,容重≥750g/L", Price = 2.42m },
|
||||
new() { Name = "玉米", Category = "粮食", Unit = "公斤", Spec = "水分≤14%,杂质≤1%", Price = 2.18m },
|
||||
new() { Name = "水稻", Category = "粮食", Unit = "公斤", Spec = "出糙率≥77%", Price = 2.68m },
|
||||
new() { Name = "花生", Category = "油料", Unit = "公斤", Spec = "果仁饱满,出仁率≥70%", Price = 7.80m },
|
||||
new() { Name = "大蒜", Category = "蔬菜", Unit = "公斤", Spec = "5cm以上,干度达标", Price = 4.50m },
|
||||
new() { Name = "西红柿", Category = "蔬菜", Unit = "公斤", Spec = "一级果,单果150g以上", Price = 2.60m },
|
||||
new() { Name = "苹果", Category = "水果", Unit = "公斤", Spec = "红富士,直径75mm以上", Price = 5.20m },
|
||||
new() { Name = "棉花", Category = "经济作物", Unit = "公斤", Spec = "衣分≥38%", Price = 15.60m }
|
||||
};
|
||||
db.Products.AddRange(products);
|
||||
db.SaveChanges();
|
||||
|
||||
// ---------- 农户 ----------
|
||||
var farmers = new List<Farmer>
|
||||
{
|
||||
new() { Name = "刘老汉", IdCard = "410122197003120011", Gender = "男", Phone = "13911110001", Village = "城东村一组", Address = "城东村一组12号", BankName = "中牟农商银行", BankAccount = "6228480012345678901", CreditScore = 92 },
|
||||
new() { Name = "陈秀英", IdCard = "410122196806250022", Gender = "女", Phone = "13911110002", Village = "城东村二组", Address = "城东村二组8号", BankName = "中牟农商银行", BankAccount = "6228480012345678902", CreditScore = 88 },
|
||||
new() { Name = "张铁柱", IdCard = "410122198204170033", Gender = "男", Phone = "13911110003", Village = "城西村一组", Address = "城西村一组21号", BankName = "农业银行", BankAccount = "6228480012345678903", CreditScore = 85 },
|
||||
new() { Name = "王翠花", IdCard = "410122197512080044", Gender = "女", Phone = "13911110004", Village = "城西村三组", Address = "城西村三组5号", BankName = "邮政储蓄银行", BankAccount = "6228480012345678904", CreditScore = 90 },
|
||||
new() { Name = "赵大勇", IdCard = "410122199003300055", Gender = "男", Phone = "13911110005", Village = "官渡镇贾庄村", Address = "贾庄村16号", BankName = "中牟农商银行", BankAccount = "6228480012345678905", CreditScore = 82 },
|
||||
new() { Name = "孙秀兰", IdCard = "410122196511150066", Gender = "女", Phone = "13911110006", Village = "韩寺镇马家村", Address = "马家村9号", BankName = "农业银行", BankAccount = "6228480012345678906", CreditScore = 78 },
|
||||
new() { Name = "李保田", IdCard = "410122197807210077", Gender = "男", Phone = "13911110007", Village = "刁家乡水沱寨村", Address = "水沱寨村3号", BankName = "邮政储蓄银行", BankAccount = "6228480012345678907", CreditScore = 86 },
|
||||
new() { Name = "周桂芳", IdCard = "410122198906060088", Gender = "女", Phone = "13911110008", Village = "官渡镇前于村", Address = "前于村28号", BankName = "中牟农商银行", BankAccount = "6228480012345678908", CreditScore = 91 }
|
||||
};
|
||||
db.Farmers.AddRange(farmers);
|
||||
db.SaveChanges();
|
||||
|
||||
// ---------- 历史收购单(近30天,用于报表与大屏演示) ----------
|
||||
var rnd = new Random(20260812);
|
||||
var purchaserIds = new[] { company.Id, stationEast.Id, stationWest.Id, individual.Id };
|
||||
var productIds = products.Select(p => p.Id).ToArray();
|
||||
var farmerIds = farmers.Select(f => f.Id).ToArray();
|
||||
var userIds = users.Select(u => u.Id).ToArray();
|
||||
|
||||
var orders = new List<PurchaseOrder>();
|
||||
for (int day = 29; day >= 0; day--)
|
||||
{
|
||||
int count = rnd.Next(3, 8);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var createAt = now.Date.AddDays(-day).AddHours(rnd.Next(7, 18)).AddMinutes(rnd.Next(0, 59));
|
||||
var productId = productIds[rnd.Next(productIds.Length)];
|
||||
var gross = rnd.Next(5000, 32000);
|
||||
var tare = rnd.Next(3200, 9000);
|
||||
var net = gross - tare;
|
||||
if (net <= 0) continue;
|
||||
var price = products.First(p => p.Id == productId).Price + (decimal)rnd.Next(-10, 10) / 10m;
|
||||
|
||||
orders.Add(new PurchaseOrder
|
||||
{
|
||||
OrderNo = $"CG{createAt:yyyyMMdd}-{(day * 10 + i):D4}",
|
||||
FarmerId = farmerIds[rnd.Next(farmerIds.Length)],
|
||||
ProductId = productId,
|
||||
PurchaserOrgId = purchaserIds[rnd.Next(purchaserIds.Length)],
|
||||
Grade = new[] { "一等", "二等", "三等" }[rnd.Next(3)],
|
||||
Unit = "公斤",
|
||||
UnitPrice = price,
|
||||
GrossWeight = gross,
|
||||
TareWeight = tare,
|
||||
NetWeight = net,
|
||||
Amount = Math.Round(net * price, 2),
|
||||
WeighCount = 2,
|
||||
Status = PurchaseStatus.Completed,
|
||||
OperatorId = userIds[rnd.Next(userIds.Length)],
|
||||
WeighInAt = createAt,
|
||||
WeighOutAt = createAt.AddMinutes(rnd.Next(15, 50)),
|
||||
CreatedAt = createAt,
|
||||
UpdatedAt = createAt
|
||||
});
|
||||
}
|
||||
}
|
||||
db.PurchaseOrders.AddRange(orders);
|
||||
db.SaveChanges();
|
||||
|
||||
// ---------- 历史付款(对应部分订单) ----------
|
||||
var payOrders = orders.Where(o => o.Status == PurchaseStatus.Completed)
|
||||
.OrderBy(o => o.CreatedAt).Take(120).ToList();
|
||||
var methods = new[] { PayMethod.Wechat, PayMethod.Alipay, PayMethod.BankTransfer, PayMethod.Cash };
|
||||
var payments = payOrders.Select((o, idx) => new PaymentRecord
|
||||
{
|
||||
PayNo = $"ZF{o.CreatedAt:yyyyMMdd}-{idx + 1:D4}",
|
||||
PurchaseOrderId = o.Id,
|
||||
FarmerId = o.FarmerId,
|
||||
Amount = o.Amount,
|
||||
Method = methods[rnd.Next(methods.Length)],
|
||||
Status = PayStatus.Success,
|
||||
TradeNo = $"TRADE{rnd.Next(100000000, 999999999)}",
|
||||
OperatorId = o.OperatorId,
|
||||
PaidAt = o.WeighOutAt,
|
||||
CreatedAt = o.WeighOutAt!.Value,
|
||||
Notes = $"收购单 {o.OrderNo} 货款结算"
|
||||
}).ToList();
|
||||
db.PaymentRecords.AddRange(payments);
|
||||
db.SaveChanges();
|
||||
|
||||
// ---------- 历史发票(对应部分订单,反向开票) ----------
|
||||
var invoiceOrders = orders.Where(o => o.Status == PurchaseStatus.Completed)
|
||||
.OrderByDescending(o => o.CreatedAt).Take(80).ToList();
|
||||
var invoices = invoiceOrders.Select((o, idx) => new Invoice
|
||||
{
|
||||
InvoiceNo = $"FP{o.CreatedAt:yyyyMMdd}-{idx + 1:D4}",
|
||||
InvoiceKind = "农产品收购发票",
|
||||
FarmerId = o.FarmerId,
|
||||
PurchaserOrgId = o.PurchaserOrgId,
|
||||
PurchaseOrderId = o.Id,
|
||||
ProductId = o.ProductId,
|
||||
Quantity = o.NetWeight,
|
||||
Unit = o.Unit,
|
||||
UnitPrice = o.UnitPrice,
|
||||
Amount = o.Amount,
|
||||
TaxRate = 0m,
|
||||
TaxAmount = 0m,
|
||||
IssueDate = o.WeighOutAt ?? o.CreatedAt,
|
||||
Status = InvoiceStatus.Issued,
|
||||
OperatorId = o.OperatorId,
|
||||
CreatedAt = o.WeighOutAt ?? o.CreatedAt
|
||||
}).ToList();
|
||||
db.Invoices.AddRange(invoices);
|
||||
db.SaveChanges();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace AgriculturalPlatform.Api.Data;
|
||||
|
||||
/// <summary>
|
||||
/// 兼容已存在的数据库:EnsureCreated 不会为已存在的表补列,
|
||||
/// 这里通过 information_schema 检测后逐列执行 ALTER TABLE,
|
||||
/// 为 Farmers 表补充四级行政区划等新列(兼容 MySQL 5.7 / 8.0 各版本)。
|
||||
/// </summary>
|
||||
public static class SchemaMigrator
|
||||
{
|
||||
public static async Task EnsureColumnsAsync(AppDbContext db)
|
||||
{
|
||||
var existing = new HashSet<string>(
|
||||
await db.Database
|
||||
.SqlQuery<string>($"SELECT COLUMN_NAME AS Value FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'Farmers'")
|
||||
.ToListAsync(),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
(string Name, string Ddl)[] columns =
|
||||
[
|
||||
("Province", "Province NVARCHAR(50) NULL"),
|
||||
("County", "County NVARCHAR(50) NULL"),
|
||||
("Township", "Township NVARCHAR(50) NULL"),
|
||||
("GroupName", "GroupName NVARCHAR(50) NULL"),
|
||||
("FarmerType", "FarmerType NVARCHAR(20) NULL"),
|
||||
("IdCardFrontUrl", "IdCardFrontUrl NVARCHAR(500) NULL"),
|
||||
("IdCardBackUrl", "IdCardBackUrl NVARCHAR(500) NULL"),
|
||||
("AvatarUrl", "AvatarUrl NVARCHAR(500) NULL"),
|
||||
];
|
||||
|
||||
foreach (var col in columns)
|
||||
{
|
||||
if (existing.Contains(col.Name)) continue;
|
||||
await db.Database.ExecuteSqlRawAsync($"ALTER TABLE Farmers ADD COLUMN {col.Ddl}");
|
||||
}
|
||||
|
||||
// 历史数据的新列为 NULL,EF 映射为不可空 string 读取会抛 DBNull 异常,统一刷新为空串(幂等)
|
||||
await db.Database.ExecuteSqlRawAsync(
|
||||
"UPDATE Farmers SET Province = COALESCE(Province,''), County = COALESCE(County,''), " +
|
||||
"Township = COALESCE(Township,''), GroupName = COALESCE(GroupName,''), " +
|
||||
"FarmerType = COALESCE(FarmerType,'农户'), " +
|
||||
"IdCardFrontUrl = COALESCE(IdCardFrontUrl,''), IdCardBackUrl = COALESCE(IdCardBackUrl,''), " +
|
||||
"AvatarUrl = COALESCE(AvatarUrl,'')");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 确保 Regions(行政区划)表存在。
|
||||
/// 表结构与 scripts/areacity/import-regions.mjs 导入脚本一致;
|
||||
/// 数据由该脚本导入(全国省市区乡镇四级,约 4.2 万条)。
|
||||
/// </summary>
|
||||
public static async Task EnsureRegionsTableAsync(AppDbContext db)
|
||||
{
|
||||
await db.Database.ExecuteSqlRawAsync("""
|
||||
CREATE TABLE IF NOT EXISTS Regions (
|
||||
Id INT NOT NULL PRIMARY KEY,
|
||||
Pid INT NOT NULL,
|
||||
Deep INT NOT NULL,
|
||||
Name VARCHAR(100) NOT NULL,
|
||||
ExtName VARCHAR(100) NOT NULL,
|
||||
Pinyin VARCHAR(200) NOT NULL DEFAULT '',
|
||||
PinyinPrefix VARCHAR(10) NOT NULL DEFAULT '',
|
||||
Hot INT NOT NULL DEFAULT 0,
|
||||
KEY idx_regions_pid (Pid),
|
||||
KEY idx_regions_deep (Deep)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
|
||||
""");
|
||||
// 老库补充 Hot 热度列(幂等)
|
||||
var hotExists = await db.Database.SqlQuery<int>(
|
||||
$"SELECT COUNT(*) AS Value FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'Regions' AND COLUMN_NAME = 'Hot'")
|
||||
.FirstOrDefaultAsync();
|
||||
if (hotExists == 0)
|
||||
await db.Database.ExecuteSqlRawAsync("ALTER TABLE Regions ADD COLUMN Hot INT NOT NULL DEFAULT 0");
|
||||
|
||||
// 常用省份热度值(值越大越靠前,幂等可重复执行)
|
||||
(string Name, int Hot)[] hotProvinces =
|
||||
[
|
||||
("四川省", 20), ("广西壮族自治区", 19), ("陕西省", 18), ("重庆市", 17), ("安徽省", 16),
|
||||
("云南省", 15), ("贵州省", 14), ("河南省", 13), ("山东省", 12), ("湖北省", 11), ("湖南省", 10),
|
||||
("甘肃省", 9), ("新疆维吾尔自治区", 8), ("江西省", 7), ("福建省", 6), ("广东省", 5),
|
||||
("河北省", 4), ("山西省", 3), ("江苏省", 2), ("浙江省", 1),
|
||||
];
|
||||
foreach (var p in hotProvinces)
|
||||
await db.Database.ExecuteSqlRawAsync(
|
||||
"UPDATE Regions SET Hot = {0} WHERE Deep = 0 AND ExtName = {1}", p.Hot, p.Name);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user