Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4c7783884c | |||
| 8ce0b3e3e8 |
@@ -11,6 +11,28 @@ namespace Meezi.Infrastructure.Data;
|
|||||||
/// <summary>Seeds 30 Persian showcase cafés for public discover (development only).</summary>
|
/// <summary>Seeds 30 Persian showcase cafés for public discover (development only).</summary>
|
||||||
public static class DiscoverShowcaseSeeder
|
public static class DiscoverShowcaseSeeder
|
||||||
{
|
{
|
||||||
|
// Approximate city centres. Each café is scattered around its city with a
|
||||||
|
// small deterministic offset (derived from its id) so the marketing map
|
||||||
|
// shows a realistic cluster of blinking lights instead of one stacked dot.
|
||||||
|
private static readonly Dictionary<string, (double Lat, double Lng, double Spread)> CityGeo = new()
|
||||||
|
{
|
||||||
|
["تهران"] = (35.70, 51.39, 0.13),
|
||||||
|
["کرج"] = (35.83, 50.99, 0.07),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static (double Lat, double Lng) GeoFor(string id, string city)
|
||||||
|
{
|
||||||
|
var (lat, lng, spread) = CityGeo.TryGetValue(city, out var g) ? g : (35.70, 51.39, 0.13);
|
||||||
|
unchecked
|
||||||
|
{
|
||||||
|
var h = 17;
|
||||||
|
foreach (var ch in id) h = (h * 31) + ch;
|
||||||
|
var ox = (((h & 0xFFFF) / 65535.0) - 0.5) * 2 * spread;
|
||||||
|
var oy = ((((h >> 16) & 0xFFFF) / 65535.0) - 0.5) * 2 * spread;
|
||||||
|
return (Math.Round(lat + oy, 5), Math.Round(lng + ox, 5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static readonly string[] ReviewAuthors = ["سارا", "علی", "مینا", "رضا", "نازنین"];
|
private static readonly string[] ReviewAuthors = ["سارا", "علی", "مینا", "رضا", "نازنین"];
|
||||||
private static readonly string[] ReviewComments =
|
private static readonly string[] ReviewComments =
|
||||||
[
|
[
|
||||||
@@ -27,6 +49,7 @@ public static class DiscoverShowcaseSeeder
|
|||||||
foreach (var spec in DiscoverShowcaseCatalog.Cafes)
|
foreach (var spec in DiscoverShowcaseCatalog.Cafes)
|
||||||
{
|
{
|
||||||
var cafe = await db.Cafes.FirstOrDefaultAsync(c => c.Id == spec.Id);
|
var cafe = await db.Cafes.FirstOrDefaultAsync(c => c.Id == spec.Id);
|
||||||
|
var (geoLat, geoLng) = GeoFor(spec.Id, spec.City);
|
||||||
if (cafe is null)
|
if (cafe is null)
|
||||||
{
|
{
|
||||||
cafe = new Cafe
|
cafe = new Cafe
|
||||||
@@ -37,6 +60,8 @@ public static class DiscoverShowcaseSeeder
|
|||||||
Slug = spec.Slug,
|
Slug = spec.Slug,
|
||||||
City = spec.City,
|
City = spec.City,
|
||||||
Address = spec.Address,
|
Address = spec.Address,
|
||||||
|
Latitude = geoLat,
|
||||||
|
Longitude = geoLng,
|
||||||
Description = spec.Description,
|
Description = spec.Description,
|
||||||
PlanTier = spec.PlanTier,
|
PlanTier = spec.PlanTier,
|
||||||
PreferredLanguage = "fa",
|
PreferredLanguage = "fa",
|
||||||
@@ -100,6 +125,12 @@ public static class DiscoverShowcaseSeeder
|
|||||||
cafe.IsVerified = true;
|
cafe.IsVerified = true;
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
if (cafe.Latitude is null || cafe.Longitude is null)
|
||||||
|
{
|
||||||
|
cafe.Latitude = geoLat;
|
||||||
|
cafe.Longitude = geoLng;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
if (changed)
|
if (changed)
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ public static class PlatformDataSeeder
|
|||||||
// fresh deploy. Idempotent. Phone is overridable via "Seed:SystemAdminPhone".
|
// fresh deploy. Idempotent. Phone is overridable via "Seed:SystemAdminPhone".
|
||||||
await EnsureOwnerAdminAsync(db, config, logger);
|
await EnsureOwnerAdminAsync(db, config, logger);
|
||||||
|
|
||||||
|
// Production-safe: give cafés without a map pin an approximate location
|
||||||
|
// from their city, so the public map lights up. Idempotent (fills nulls).
|
||||||
|
await BackfillCafeLocationsAsync(db, logger);
|
||||||
|
|
||||||
if (!env.IsDevelopment())
|
if (!env.IsDevelopment())
|
||||||
{
|
{
|
||||||
// Production: also ensure integration settings (Kavenegar enabled/template,
|
// Production: also ensure integration settings (Kavenegar enabled/template,
|
||||||
@@ -45,6 +49,79 @@ public static class PlatformDataSeeder
|
|||||||
await EnsureIntegrationSettingsAsync(db, logger);
|
await EnsureIntegrationSettingsAsync(db, logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Approximate centres for the major Iranian cities cafés sign up from.
|
||||||
|
private static readonly Dictionary<string, (double Lat, double Lng)> CityCentres = new(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
["تهران"] = (35.70, 51.39),
|
||||||
|
["کرج"] = (35.84, 50.99),
|
||||||
|
["مشهد"] = (36.30, 59.61),
|
||||||
|
["اصفهان"] = (32.66, 51.67),
|
||||||
|
["شیراز"] = (29.59, 52.53),
|
||||||
|
["تبریز"] = (38.08, 46.29),
|
||||||
|
["قم"] = (34.64, 50.88),
|
||||||
|
["اهواز"] = (31.32, 48.67),
|
||||||
|
["کرمانشاه"] = (34.31, 47.07),
|
||||||
|
["رشت"] = (37.28, 49.58),
|
||||||
|
["ارومیه"] = (37.55, 45.07),
|
||||||
|
["همدان"] = (34.80, 48.52),
|
||||||
|
["یزد"] = (31.90, 54.37),
|
||||||
|
["اراک"] = (34.09, 49.69),
|
||||||
|
["کرمان"] = (30.28, 57.08),
|
||||||
|
["بندرعباس"] = (27.18, 56.27),
|
||||||
|
["قزوین"] = (36.28, 50.00),
|
||||||
|
["ساری"] = (36.57, 53.06),
|
||||||
|
["گرگان"] = (36.84, 54.44),
|
||||||
|
["زنجان"] = (36.68, 48.49),
|
||||||
|
["کیش"] = (26.56, 53.98),
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gives cafés that have no map pin an approximate location at their city
|
||||||
|
/// centre (plus a small deterministic per-café offset so multiple cafés in
|
||||||
|
/// one city don't stack on a single point). Only fills rows where Latitude or
|
||||||
|
/// Longitude is null and the city is recognised; owners can drop an exact pin
|
||||||
|
/// later from Settings. Idempotent — never overwrites an existing pin.
|
||||||
|
/// </summary>
|
||||||
|
private static async Task BackfillCafeLocationsAsync(AppDbContext db, ILogger logger)
|
||||||
|
{
|
||||||
|
var cafes = await db.Cafes
|
||||||
|
.Where(c => c.DeletedAt == null
|
||||||
|
&& (c.Latitude == null || c.Longitude == null)
|
||||||
|
&& c.City != null)
|
||||||
|
.ToListAsync();
|
||||||
|
if (cafes.Count == 0) return;
|
||||||
|
|
||||||
|
var updated = 0;
|
||||||
|
foreach (var cafe in cafes)
|
||||||
|
{
|
||||||
|
var city = cafe.City!.Trim();
|
||||||
|
if (!CityCentres.TryGetValue(city, out var centre)) continue;
|
||||||
|
var (lat, lng) = ScatterAround(cafe.Id, centre.Lat, centre.Lng, 0.05);
|
||||||
|
cafe.Latitude = lat;
|
||||||
|
cafe.Longitude = lng;
|
||||||
|
updated++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updated > 0)
|
||||||
|
{
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
logger.LogInformation(
|
||||||
|
"Cafe location backfill: set approximate coordinates for {Count} café(s) from city centre", updated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (double Lat, double Lng) ScatterAround(string id, double lat, double lng, double spread)
|
||||||
|
{
|
||||||
|
unchecked
|
||||||
|
{
|
||||||
|
var h = 17;
|
||||||
|
foreach (var ch in id) h = (h * 31) + ch;
|
||||||
|
var ox = (((h & 0xFFFF) / 65535.0) - 0.5) * 2 * spread;
|
||||||
|
var oy = ((((h >> 16) & 0xFFFF) / 65535.0) - 0.5) * 2 * spread;
|
||||||
|
return (Math.Round(lat + oy, 5), Math.Round(lng + ox, 5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ensures the platform owner's system-admin account exists in EVERY environment
|
/// Ensures the platform owner's system-admin account exists in EVERY environment
|
||||||
/// (including production), so the admin panel is reachable on a fresh deploy.
|
/// (including production), so the admin panel is reachable on a fresh deploy.
|
||||||
|
|||||||
Reference in New Issue
Block a user