ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

微信小程序+NET Core+layui多店铺商城实战指南

2026/10/7 20:14:47 拓冰建站 浏览量
微信小程序+NET Core+layui多店铺商城实战指南 简介这是一套基于微信小程序、.NET Core与Layui构建的多店铺商城系统面向C#开发者及新零售电商项目实践者提供可商用的全栈解决方案。资源涵盖小程序前端、NetCore WebAPI后端、Layui管理后台及插件扩展模块覆盖商户入驻、商品管理、订单处理、数据导出等核心业务场景适合中高级开发者学习架构设计与跨端协同开发。压缩包共1915个文件主体为798个C#业务逻辑文件如OrderController、GoodsController、180个JS交互脚本、148个CSHTML视图页、42个WXML/WXSS小程序组件辅以配置文件、静态资源与DLL依赖整体体积10.57MB结构清晰、模块解耦度高。已有233人学习下载读者可直接获取完整项目源码、分层目录结构、插件化扩展机制及典型业务服务实现如订单处理、用户权限、插件管理快速理解电商系统从接口到界面的落地细节。1. 微信小程序 .NET Core layui 多店铺商城不是拼凑三件套而是用 C# 构建可交付、可运维、能过审的商用闭环你手头有一套标着「商用标准」的多店铺商城源码技术栈写着微信小程序 .NET Core layui —— 看起来很稳但真往生产环境一推就卡在登录态同步失败、订单状态不一致、小程序审核被拒、后台管理页加载慢得像在等编译完成。这不是技术选型错了而是没理清这三者的真实分工微信小程序是用户触点与前端交互层含登录、支付、地址、分享.NET Core 是业务中枢与数据守门人多租户隔离、库存扣减、分账逻辑、微信支付回调验签layui 是后台管理系统的快速落地界面非展示型页面而是运营、店员、财务真正每天点几十次的操作台。它不追求炫酷动效但必须扛住 30 店铺同时上新、秒杀、售后批量处理它不靠 Vue/React 生态堆功能而是用 C# 的强类型、EF Core 的迁移可控性、JWT 的细粒度权限控制把「多店铺」从概念落到数据库租户字段、API 路由前缀、日志隔离策略里。适合正在接手这类项目的技术负责人、独立开发者或需要向甲方交付「有后台、有小程序、有管理端、能上线」完整体的中小团队——别再拿 uni-app 套壳当商城也别用 PHP 快速原型应付验收。2. 搭建可验证的最小运行体系从 .NET Core API 到小程序登录链路打通要让这套体系活起来第一步不是写商品列表而是跑通「用户在小程序里点登录 → 后台拿到 openid/session_key → 返回自定义 token → 小程序后续请求带 token 访问订单接口」这条主干链路。很多团队卡在这一步不是代码写错而是对微信登录机制和 .NET Core 中间件协作的理解有断层。2.1 初始化 .NET Core Web API 项目并启用 JWT 认证我们不用模板里的dotnet new webapi全家桶而是从干净的空项目起步确保每个包都清楚用途dotnet new web -n ShopAdminApi cd ShopAdminApi dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer dotnet add package Microsoft.IdentityModel.Tokens dotnet add package System.IdentityModel.Tokens.Jwt dotnet add package Microsoft.EntityFrameworkCore.SqlServer dotnet add package Microsoft.EntityFrameworkCore.Tools提示这里不装Swashbuckle.AspNetCoreSwagger因为商用后台 API 文档应由 Postman Collection 或 OpenAPI 3.0 YAML 手动维护避免 Swagger UI 暴露敏感路径。.NET Core 过滤器在此阶段主要用于全局异常捕获和日志埋点而非权限控制——权限交给 JWT Bearer 验证中间件统一处理。在Program.cs中配置服务var builder WebApplication.CreateBuilder(args); // 1. 添加 JWT 认证服务 builder.Services.AddAuthentication(Bearer) .AddJwtBearer(Bearer, options { options.TokenValidationParameters new TokenValidationParameters { ValidateIssuerSigningKey true, IssuerSigningKey new SymmetricSecurityKey( Encoding.UTF8.GetBytes(builder.Configuration[Jwt:Key] ?? your-32-byte-secret-key-here)), ValidateIssuer false, ValidateAudience false, ClockSkew TimeSpan.Zero // 关键微信返回时间戳可能有微小偏差设为零避免验签失败 }; }); // 2. 添加授权服务必须在 AddAuthentication 之后 builder.Services.AddAuthorization(); // 3. 添加控制器支持 builder.Services.AddControllers(); var app builder.Build(); // 4. 启用认证 授权中间件顺序不能错 app.UseAuthentication(); // 必须在 UseAuthorization 之前 app.UseAuthorization(); app.MapControllers(); app.Run();appsettings.json中添加 JWT 配置段{ Jwt: { Key: your-32-byte-secret-key-here }, Logging: { LogLevel: { Default: Information, Microsoft.AspNetCore: Warning } } }2.2 实现微信小程序登录接口接收 code调用微信接口换取 openid并签发 JWT token新建控制器Controllers/AuthController.cs[ApiController] [Route(api/[controller])] public class AuthController : ControllerBase { private readonly IHttpClientFactory _httpClientFactory; private readonly IConfiguration _configuration; public AuthController(IHttpClientFactory httpClientFactory, IConfiguration configuration) { _httpClientFactory httpClientFactory; _configuration configuration; } [HttpPost(login-by-code)] public async TaskIActionResult LoginByCode([FromBody] LoginByCodeRequest request) { if (string.IsNullOrWhiteSpace(request.Code)) return BadRequest(new { message code 不能为空 }); // 1. 调用微信接口换取 session_key 和 openid var appId _configuration[WeChat:AppId]; var appSecret _configuration[WeChat:AppSecret]; var url $https://api.weixin.qq.com/sns/jscode2session? $appid{appId}secret{appSecret}js_code{request.Code}grant_typeauthorization_code; var client _httpClientFactory.CreateClient(); var response await client.GetAsync(url); var json await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { return StatusCode((int)response.StatusCode, new { message 微信接口调用失败, raw json }); } var wxResult JsonSerializer.DeserializeWeChatSessionResponse(json); if (wxResult null || string.IsNullOrEmpty(wxResult.Openid)) { return BadRequest(new { message 获取 openid 失败, detail json }); } // 2. 根据 openid 查询或创建用户此处简化直接生成 token实际需查库 var token GenerateJwtToken(wxResult.Openid, wxResult.Unionid ?? wxResult.Openid); return Ok(new { success true, token, expires_in 3600, openid wxResult.Openid, unionid wxResult.Unionid }); } private string GenerateJwtToken(string openid, string unionid) { var key Encoding.UTF8.GetBytes(_configuration[Jwt:Key]); var claims new[] { new Claim(JwtRegisteredClaimNames.Sub, openid), new Claim(unionid, unionid), new Claim(iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString()), new Claim(exp, DateTimeOffset.UtcNow.AddHours(1).ToUnixTimeSeconds().ToString()) }; var token new JwtSecurityToken( issuer: shop-admin-api, audience: mini-program-client, claims: claims, expires: DateTime.UtcNow.AddHours(1), signingCredentials: new SigningCredentials( new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256) ); return new JwtSecurityTokenHandler().WriteToken(token); } } public class LoginByCodeRequest { public string Code { get; set; } } public class WeChatSessionResponse { public string Openid { get; set; } public string Session_Key { get; set; } public string Unionid { get; set; } public string Errmsg { get; set; } }逻辑说明LoginByCodeRequest是小程序传来的code必须严格校验非空微信jscode2session接口返回的是openid唯一标识该用户在当前公众号/小程序下的身份和unionid跨公众号/小程序的统一 ID需绑定开放平台GenerateJwtToken中iatissued at和expexpires必须用 Unix 时间戳且ClockSkew TimeSpan.Zero已在 Startup 中设置否则因服务器时间与微信服务器微小差异导致验签失败实际项目中此处应查询User表若 openid 不存在则插入新用户含默认店铺 ID、注册时间、设备信息若存在则更新最后登录时间。2.3 小程序端调用登录接口并持久化 token在小程序app.js的onLaunch中完成自动登录App({ onLaunch() { wx.login({ success: res { if (res.code) { // 1. 发起网络请求将 code 传给后端 wx.request({ url: https://your-api-domain.com/api/auth/login-by-code, method: POST, data: { code: res.code }, success: loginRes { if (loginRes.data.success) { // 2. 将 token 存入 storage后续所有请求带上 wx.setStorageSync(auth_token, loginRes.data.token); // 3. 触发全局登录成功事件供页面监听 this.globalData.isLoggedIn true; this.globalData.token loginRes.data.token; } else { console.error(登录失败, loginRes.data); } }, fail: err console.error(API 请求失败, err) }); } else { console.error(登录失败 res.errMsg); } } }); }, globalData: { isLoggedIn: false, token: } });在任意页面如pages/index/index.js发起受保护接口请求时wx.request({ url: https://your-api-domain.com/api/orders, header: { Authorization: Bearer wx.getStorageSync(auth_token) }, success: res { console.log(订单列表, res.data); } });参数说明小程序wx.request的header.Authorization必须为Bearer token格式大小写敏感.NET Core 过滤器可在此处扩展例如在AuthorizationFilter中检查HttpContext.User.Identity.IsAuthenticated未通过则返回 401此链路已覆盖「微信小程序登录获取手机号」的前置条件——只有登录态有效才能调用wx.getPhoneNumber获取加密数据后续解密需在 .NET Core 后端用session_key完成不在本节展开。3. 多店铺核心模型设计用 EF Core 实现租户隔离与数据路由“多店铺”不是简单加个ShopId字段就完事。商用系统要求A 店铺看不到 B 店铺的订单、商品、客户运营后台能按店铺筛选数据数据库层面具备横向扩展能力未来单库撑不住时可分库。EF Core 本身不提供多租户但可通过查询全局过滤器Global Query Filters 自定义 DbContext 租户解析中间件三层联动实现。3.1 定义基础实体与租户抽象基类创建Models/Entities/BaseEntity.cspublic abstract class BaseEntity { public Guid Id { get; set; } Guid.NewGuid(); public DateTime CreatedAt { get; set; } DateTime.UtcNow; public DateTime? UpdatedAt { get; set; } } public abstract class TenantEntity : BaseEntity { public Guid ShopId { get; set; } // 所属店铺 ID }定义具体业务实体继承TenantEntitypublic class Product : TenantEntity { public string Name { get; set; } string.Empty; public decimal Price { get; set; } public int Stock { get; set; } public string Sku { get; set; } string.Empty; } public class Order : TenantEntity { public string OrderNo { get; set; } string.Empty; public Guid UserId { get; set; } public decimal TotalAmount { get; set; } public OrderStatus Status { get; set; } } public enum OrderStatus { Pending 0, Paid 1, Shipped 2, Completed 3, Cancelled 4 }3.2 创建租户上下文与动态 ShopId 解析新建Data/ShopDbContext.cspublic class ShopDbContext : DbContext { private readonly IHttpContextAccessor _httpContextAccessor; public ShopDbContext(DbContextOptionsShopDbContext options, IHttpContextAccessor httpContextAccessor) : base(options) { _httpContextAccessor httpContextAccessor; } public DbSetProduct Products { get; set; } public DbSetOrder Orders { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { // 1. 为所有 TenantEntity 添加全局过滤器 foreach (var entityType in modelBuilder.Model.GetEntityTypes() .Where(e e.ClrType.IsSubclassOf(typeof(TenantEntity)))) { modelBuilder.Entity(entityType.ClrType) .HasQueryFilter(CreateTenantFilter(entityType.ClrType)); } // 2. 配置主键、索引等略 modelBuilder.EntityProduct().HasKey(p p.Id); modelBuilder.EntityOrder().HasKey(o o.Id); modelBuilder.EntityProduct().HasIndex(p p.ShopId); modelBuilder.EntityOrder().HasIndex(o o.ShopId); } private LambdaExpression CreateTenantFilter(Type entityType) { var parameter Expression.Parameter(entityType, e); var shopIdProperty Expression.Property(parameter, nameof(TenantEntity.ShopId)); var currentShopId GetCurrentShopId(); // 生成表达式e e.ShopId currentShopId var constant Expression.Constant(currentShopId, typeof(Guid)); var equal Expression.Equal(shopIdProperty, constant); return Expression.Lambda(equal, parameter); } private Guid GetCurrentShopId() { // 从 HttpContext 中提取 ShopId可来自 JWT claim、Header、或路由参数 var context _httpContextAccessor.HttpContext; if (context?.User.Identity.IsAuthenticated true) { var shopIdClaim context.User.Claims.FirstOrDefault(c c.Type shop_id); if (Guid.TryParse(shopIdClaim?.Value, out var shopId)) return shopId; } // 开发环境兜底取第一个店铺仅限本地调试 return Guid.Parse(00000000-0000-0000-0000-000000000001); } }注意IHttpContextAccessor必须在Program.cs中注册builder.Services.AddHttpContextAccessor(); builder.Services.AddDbContextShopDbContext(options options.UseSqlServer(builder.Configuration.GetConnectionString(DefaultConnection)));3.3 实现租户解析中间件从 JWT Token 中提取 shop_id 并注入 Claims新建Middleware/TenantResolutionMiddleware.cspublic class TenantResolutionMiddleware { private readonly RequestDelegate _next; public TenantResolutionMiddleware(RequestDelegate next) { _next next; } public async Task InvokeAsync(HttpContext context) { var authHeader context.Request.Headers.Authorization.ToString(); if (authHeader.StartsWith(Bearer , StringComparison.OrdinalIgnoreCase)) { var token authHeader[Bearer .Length..].Trim(); try { var tokenHandler new JwtSecurityTokenHandler(); var jwtToken tokenHandler.ReadJwtToken(token); var shopIdClaim jwtToken.Claims.FirstOrDefault(c c.Type shop_id); if (shopIdClaim ! null Guid.TryParse(shopIdClaim.Value, out var shopId)) { // 将 shop_id 注入到 User.Claims供 DbContext 使用 var identity context.User.Identity as ClaimsIdentity; if (identity ! null) { identity.AddClaim(new Claim(shop_id, shopId.ToString())); } } } catch (Exception ex) { // 日志记录但不中断流程 Console.WriteLine($Tenant resolution failed: {ex.Message}); } } await _next(context); } }在Program.cs中注册中间件放在UseAuthentication()之后UseAuthorization()之前app.UseAuthentication(); app.UseMiddlewareTenantResolutionMiddleware(); // ← 新增 app.UseAuthorization();逻辑说明全局过滤器HasQueryFilter是 EF Core 多租户的核心它会在每一条生成的 SQL 查询语句末尾自动追加AND [ShopId] p0无需在每个 Repository 方法里手动.Where(x x.ShopId shopId)TenantResolutionMiddleware的作用是补全 JWT 中缺失的shop_idclaim —— 实际项目中shop_id应在用户登录时由后台根据 openid 查询用户所属店铺后写入 token此设计天然支持「单店铺多管理员」不同管理员登录后其 JWT 中shop_id不同查询自动隔离若需支持「一个用户跨多个店铺」则需改用ListGuid存储shop_ids并在过滤器中改为Contains但会牺牲索引效率商用建议用角色权限控制如“超级管理员”可查全部店铺。4. layui 后台管理系统不是静态页面而是与 .NET Core API 深度协同的运营中枢很多人误以为 layui 只是“老式前端框架”其实它的价值在于极低的学习成本、开箱即用的表单/表格/弹窗组件、无构建步骤、可直接部署到 Nginx 静态目录。对于多店铺商城的运营后台它比 React/Vue 更合适——店长不需要懂 MVVM只需要点“导出本月销售报表”、“批量修改商品状态”、“查看某店铺所有售后单”。关键在于layui 页面如何安全、高效、可维护地对接 .NET Core API。4.1 构建基于 layui 的店铺管理页列表 搜索 批量操作创建wwwroot/admin/shop-list.html!DOCTYPE html html head meta charsetutf-8 title店铺管理/title link relstylesheet href/layui/css/layui.css /head body classlayui-layout-body div classlayui-layout layui-layout-admin div classlayui-header div classlayui-logo多店铺商城后台/div ul classlayui-nav layui-layout-left li classlayui-nav-itema href/admin/shop-list.html店铺管理/a/li li classlayui-nav-itema href/admin/product-list.html商品管理/a/li /ul ul classlayui-nav layui-layout-right li classlayui-nav-itema hrefjavascript:;管理员/a/li /ul /div div classlayui-body div stylepadding: 15px; div classlayui-form div classlayui-form-item label classlayui-form-label店铺名称/label div classlayui-input-inline input typetext nameshopName placeholder请输入店铺名称 classlayui-input /div button classlayui-btn lay-submit lay-filtersearch搜索/button button classlayui-btn layui-btn-primary onclicklocation.reload()重置/button /div /div table idshopTable lay-filtershopTable/table /div /div /div script src/layui/layui.js/script script layui.use([table, form, layer], function () { var table layui.table; var form layui.form; var layer layui.layer; // 渲染表格 table.render({ elem: #shopTable, height: 500, url: /api/shops, // 对应 .NET Core 的 GET /api/shops method: get, headers: { Authorization: Bearer localStorage.getItem(admin_token) // 后台管理端 token 存 localstorage }, page: true, cols: [[ {type: checkbox}, {field: id, title: ID, width: 120}, {field: name, title: 店铺名称, width: 200}, {field: contact, title: 联系人, width: 120}, {field: phone, title: 联系电话, width: 150}, {field: status, title: 状态, width: 100, templet: #statusTpl}, {field: createdAt, title: 创建时间, width: 180}, {title: 操作, width: 180, align: center, toolbar: #barDemo} ]] }); // 状态模板 layui.template(#statusTpl, function (d) { return d.status 1 ? span classlayui-badge layui-bg-green启用/span : span classlayui-badge layui-bg-gray禁用/span; }); // 搜索提交 form.on(submit(search), function (data) { table.reload(shopTable, { where: { name: data.field.shopName }, page: { curr: 1 } }); return false; }); // 工具栏事件 table.on(tool(shopTable), function (obj) { var data obj.data; if (obj.event edit) { layer.open({ type: 2, title: 编辑店铺, area: [600px, 500px], content: /admin/shop-edit.html?id data.id }); } else if (obj.event delete) { layer.confirm(确定删除, function (index) { $.ajax({ url: /api/shops/ data.id, type: DELETE, headers: { Authorization: Bearer localStorage.getItem(admin_token) }, success: function () { obj.del(); layer.close(index); layer.msg(删除成功); } }); }); } }); }); /script !-- 行内工具栏 -- script typetext/html idbarDemo a classlayui-btn layui-btn-xs lay-eventedit编辑/a a classlayui-btn layui-btn-danger layui-btn-xs lay-eventdelete删除/a /script /body /html4.2 .NET Core 后端实现店铺管理 API支持分页、搜索、软删除新建Controllers/ShopsController.cs[ApiController] [Route(api/[controller])] [Authorize] // 必须登录 public class ShopsController : ControllerBase { private readonly ShopDbContext _context; public ShopsController(ShopDbContext context) { _context context; } [HttpGet] public async TaskActionResultPagedResultShop GetShops( [FromQuery] int page 1, [FromQuery] int pageSize 10, [FromQuery] string name null) { var query _context.Shops.AsQueryable(); if (!string.IsNullOrWhiteSpace(name)) { query query.Where(s s.Name.Contains(name)); } var totalCount await query.CountAsync(); var items await query .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(); return Ok(new PagedResultShop { Items items, TotalCount totalCount, PageNumber page, PageSize pageSize }); } [HttpGet({id:guid})] public async TaskActionResultShop GetShop(Guid id) { var shop await _context.Shops.FindAsync(id); if (shop null) return NotFound(); return shop; } [HttpPut({id:guid})] public async TaskIActionResult UpdateShop(Guid id, [FromBody] ShopUpdateRequest request) { var shop await _context.Shops.FindAsync(id); if (shop null) return NotFound(); shop.Name request.Name; shop.Contact request.Contact; shop.Phone request.Phone; shop.Status request.Status; shop.UpdatedAt DateTime.UtcNow; await _context.SaveChangesAsync(); return NoContent(); } [HttpDelete({id:guid})] public async TaskIActionResult DeleteShop(Guid id) { var shop await _context.Shops.FindAsync(id); if (shop null) return NotFound(); // 软删除不物理删除只标记 shop.IsDeleted true; shop.DeletedAt DateTime.UtcNow; await _context.SaveChangesAsync(); return NoContent(); } } public class ShopUpdateRequest { public string Name { get; set; } string.Empty; public string Contact { get; set; } string.Empty; public string Phone { get; set; } string.Empty; public bool Status { get; set; } } public class PagedResultT { public ListT Items { get; set; } new(); public int TotalCount { get; set; } public int PageNumber { get; set; } public int PageSize { get; set; } }参数说明PagedResultT是自定义分页响应结构避免直接返回IActionResult导致前端无法统一处理Shop实体需继承TenantEntity因此GetShops查询会自动应用ShopId过滤器确保管理员只能看到自己店铺的数据DeleteShop使用软删除IsDeleted字段符合商用审计要求且便于恢复layui表格的headers.Authorization从localStorage读取该 token 应在管理员登录后台时由 .NET Core/api/admin/login接口签发与小程序 token 分离不同密钥、不同过期时间、不同 claim。4.3 为 layui 页面注入动态权限按钮级显隐控制layui 本身无权限系统但可结合 .NET Core 的 Role/Claim在页面加载时动态渲染按钮在ShopController.GetShops中将当前用户权限传入视图模型若用 Razor但纯静态 HTML 下需在登录成功后由后台返回权限列表存入localStorage// 登录成功后返回用户权限 return Ok(new { token jwtToken, permissions new[] { shop:read, shop:edit, product:export } });在shop-list.html的 JS 中// 加载完成后检查权限 if (!localStorage.getItem(permissions)?.includes(shop:edit)) { // 隐藏编辑按钮 $(.layui-table-box).find([lay-eventedit]).hide(); } if (!localStorage.getItem(permissions)?.includes(shop:delete)) { $(.layui-table-box).find([lay-eventdelete]).hide(); }提示权限字符串如shop:edit应与 .NET Core 的[Authorize(Policy ShopEdit)]策略名对齐形成前后端一致的权限字典。5. 商用级避坑指南那些让项目延期两周的 .NET Core 小程序 layui 组合雷区这套组合拳看似平实但实际交付中 80% 的返工都源于几个隐蔽但高频的坑。以下是我带三个团队落地同类项目后血泪整理的 5 条真实踩坑记录每一条都附带复现方式、根因分析和可立即执行的修复方案。5.1 现象小程序调用 .NET Core API 返回 401但 Postman 测试完全正常原因小程序wx.request默认不携带 Cookie而你的 .NET Core 项目启用了Cookie认证如AddCookie同时又配置了AddJwtBearer导致AuthenticationScheme冲突中间件优先尝试 Cookie 验证失败后直接返回 401根本没走到 JWT 验证逻辑。解决彻底移除AddCookie相关代码小程序场景只用 JWT Bearer 认证检查Program.cs中AddAuthentication是否指定了默认 Schemebuilder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) // ← 明确指定默认为 JWT .AddJwtBearer(...);确保小程序请求 Header 严格为Authorization: Bearer xxx空格、大小写、多余字符都会导致失败。5.2 现象layui 后台页面刷新后表格数据为空F12 查看 Network 发现/api/shops返回 401原因localStorage.getItem(admin_token)在页面 JS 执行时为null因为 token 是异步登录后才写入的而 layuitable.render是同步执行的导致请求发出时 token 还没存进去。解决不要在 HTML 中直接写script调用table.render改为在登录成功后用window.location.href /admin/shop-list.html跳转并在shop-list.html的 JS 中加防抖function loadTable() { const token localStorage.getItem(admin_token); if (!token) { setTimeout(loadTable, 100); // 每 100ms 检查一次 return; } // 此处再调用 table.render } loadTable();更优方案登录接口返回redirect_url后端生成带 token 的临时链接如/admin/shop-list.html?tokenxxx前端解析后存入localStorage再跳转。5.3 现象多店铺下A 店铺管理员在后台修改商品价格B 店铺小程序商品列表却同步更新了价格原因Product实体未继承TenantEntity或OnModelCreating中漏掉了对该实体的HasQueryFilter配置导致 EF Core 生成的 SQL 没有AND ShopId p0条件查询返回了全库数据。解决检查Product类是否: TenantEntity在ShopDbContext.OnModelCreating中打印所有被过滤的实体类型Console.WriteLine($Applying filter to: {entityType.ClrType.Name});启动项目查看日志是否输出Product若没有说明modelBuilder.Model.GetEntityTypes()未包含它检查DbSetProduct是否声明、Product是否被Assembly.Load加载。最终验证开启 EF Core SQL 日志执行context.Products.ToListAsync()确认生成的 SQL 包含WHERE [ShopId] ...。5.4 现象微信小程序审核被拒提示“页面底部导航栏不符合规范”或“顶部导航栏高度异常”原因wx.setNavigationBarColor或wx.setNavigationBarTitle在部分安卓机型上失效导致原生导航栏与自定义 tabBar 高度错位更常见的是app.json中tabBar配置的list项iconPath和selectedIconPath指向的图片尺寸不是 81×81px微信强制要求或图片格式不是 PNG 透明背景。解决彻底放弃自定义导航栏使用微信原生navigationStyle: defaulttabBar图标必须用81×81px、PNG、透明背景、无边框的图片命名如home.png/home-active.png在app.json中严格校验tabBar: { color: #666, selectedColor: #108ee9, borderStyle: black, backgroundColor: #ffffff, list: [{ pagePath: pages/index/index, text: 首页, iconPath: assets/tabbar/home.png, selectedIconPath: assets/tabbar/home-active.png }] }使用微信开发者工具「真机调试」功能在 iOS 和主流安卓机型上预览确认无错位。5.5 现象layui 后台导出 Excel 功能点击无反应控制台报错Cannot read property exportFile of undefined原因layui.table的exportFile方法是 2.8.0 版本新增的而你引入的layui.js是 2.7.x 或更旧版本或者未正确加载laydate、layexcel模块虽然文档说exportFile不依赖模块但实际需layexcel支持。解决升级 layui 至2.8.18当前最新稳定版下载地址https://www.layui.com/download.html在shop-list.html中明确加载模块script src/layui/layui.js/script script layui.config({ version: 2.8.18 // 强制版本 }).use([table, form, layer, layexcel], function () { // ... }); /script导出前检查浏览器是否支持 Blobif (typeof Blob undefined) { layer.msg(当前浏览器不支持导出功能请升级至 Chrome/Firefox/Edge); return; }6. 进阶技巧用 C# 实现微信小程序「获取手机号」的端到端解密与绑定微信小程序wx.getPhoneNumber是商用商城的刚需——用户授权后后台必须解密获得真实手机号用于注册、登录、短信通知。但网上大量教程只贴 JS 代码却忽略 .NET Core 后端解密的关键细节微信返回的加密数据encryptedData必须用用户的 session_key 解密而 session_key 有效期仅 2 小时且同一 code 只能换取一次。很多团队在这里翻车导致用户点了授权却收不到短信或解密出乱码。6.1 小程序端正确调用 getPhoneNumber 并传递 encryptedData 到后端// pages/user/bind-phone.js Page({ bindGetPhoneNumber(e) { if (!e.detail.encryptedData) { wx.showToast({ title: 授权失败, icon: none p a hrefhttps://download.csdn.net/download/qq_41701956/87943187 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p