简介:这是一套基于微信小程序与NetCore构建的多店铺商城系统,采用C#后台与layui管理界面,面向需要快速搭建商用级小程序商城的技术团队与开发者,覆盖前端小程序、管理后台、插件管理和WebApi接口层,能直接用于新零售/电商场景的二次开发与部署。压缩包共1915个文件,大小10.57MB,主要包含798个C#源码文件、148个cshtml后台视图、180个js脚本与42个wxml小程序页面,另有sql数据库脚本、配置文件及项目解决方案,目录结构与代码分层清晰便于按模块查阅;包内gif、png等素材与说明文档还可作为界面交互与效果预览参考。已有233人学习下载,资料涵盖订单处理、商品管理、用户管理、插件管理等核心模块的实现代码,能帮助理解商城从接口到前后端的完整链路。对于具备一定C#基础、希望基于成熟项目快速启动或学习小程序商城架构的开发者,这套资源具有较高参考价值。
1. 为什么说多店铺商城不是“单商户系统加个店铺字段”
一套以微信小程序 + NetCore + layui 构建、后台用 C# 写、目标是多店铺商城商用标准的项目体系,很多团队在评估时都会问同一个问题:能不能把单商户商城加个 shop_id 字段直接改成多店铺?我的结论是最好别这么干。多店铺商城和单商户系统的差别不在“店铺数量”,而在三件事:数据怎么隔离、订单怎么分账、后台权限怎么分给不同店主。社区团购、校园食堂订餐、跨区域连锁零售,需求看着不一样,落到架构上都是同一套东西——平台管店铺,店铺管商品,顾客在小程序下单,钱先到平台再按规则分到店里。下面把这条路拆开讲,从表设计到 NetCore 接口、小程序端、layui 后台,一直讲到坑,适合手里有 C# 团队、想用这套技术栈自建商用底座、又不愿意被 SaaS 服务商绑定的情况。
2. 多店铺商城的数据骨架:店铺隔离、SPU/SKU 与订单状态机
一笔订单从顾客下单到店铺结算,要穿过用户表、店铺表、商品表、订单表、支付流水表。多店铺商城一半的坑都出在“隔离字段缺了”:要么列表串店,要么分账算错。先把表设计定住,后面写接口才不会天天返工。
2.1 店铺和会员:用冗余 shop_id 而不是独立库
多店铺本质是 SaaS 租户隔离,常见做法有三种:独立数据库、独立 schema、共享库加租户 ID。自研商城我推荐第三种,理由是服务器成本低、运维简单、要拉全平台报表时一条 SQL 就能跨店聚合。隔离字段统一叫 shop_id,落库时冗余到每一张业务表,而不是靠联表推出来。
create database if not exists mall default charset utf8mb4; create table shop ( id bigint unsigned auto_increment primary key comment '店铺ID', shop_name varchar(64) not null comment '店铺名称', owner_member_id bigint unsigned not null comment '店主会员ID', status tinyint not null default 1 comment '1营业 0关闭', commission_rate decimal(5,2) not null default 10.00 comment '平台抽佣比例,10.00表示10%', created_at datetime not null default current_timestamp ) engine=InnoDB comment='店铺表'; create table member ( id bigint unsigned auto_increment primary key, shop_id bigint unsigned not null comment '所属店铺,0表示平台会员', openid varchar(64) not null comment '微信openid', unionid varchar(64) default null comment '同一主体下的用户标识', nick_name varchar(64) default '', balance decimal(12,2) not null default 0 comment '可提现余额,单位元', created_at datetime not null default current_timestamp, unique key uk_openid(openid), key idx_shop_id(shop_id) ) engine=InnoDB comment='会员表';这里有两个设计点要说明。第一,member 表直接带 shop_id,表示这个用户在哪个店铺注册;如果业务里顾客要同时关注多个店铺,再建一张 member_follow_shop 关系表,不要一来就搞复杂的中间表。第二,金额字段用 decimal,订单支付和退款都用它,后面做优惠分摊时的四舍五入规则也统一在服务端处理,绝不在前端算钱。
shop 表里我放了 commission_rate 字段,下单时把订单的抽佣比例快照进订单表。这样平台后续改店铺佣金率,不会影响已经支付的订单。
2.2 商品:SPU 与 SKU 分开,价格用整数分存储
多店铺商品模型多数团队会纠结要不要拆 SPU 和 SKU。一旦商品涉及颜色、尺码、容量这类规格组合,不拆就会到处出现“同一件商品多条重复标题和图片”的烂摊子。常见做法是:SPU 表示“同款商品”,SKU 表示“具体可下单的规格组合”。
create table spu ( id bigint unsigned auto_increment primary key, shop_id bigint unsigned not null, spu_no varchar(32) not null comment '商家商品编码', title varchar(128) not null, main_pic varchar(255) not null, status tinyint not null default 0 comment '0草稿 1上架 2下架', created_at datetime not null default current_timestamp, key idx_shop_status(shop_id, status) ) engine=InnoDB comment='商品SPU表'; create table sku ( id bigint unsigned auto_increment primary key, spu_id bigint unsigned not null, shop_id bigint unsigned not null comment '冗余店铺ID,订单和分账都要用', sku_no varchar(32) not null, spec_attrs varchar(255) not null comment '规格描述,如:颜色:黑色;尺码:M', price integer not null comment '售价,单位分', stock integer not null default 0, sale_count integer not null default 0, status tinyint not null default 1, key idx_spu(spu_id) ) engine=InnoDB comment='SKU表';price 用 integer 而不是 decimal,单位是“分”。下单、退款、分账、对账全部按分计算,最后展示再转成元。用整数避免 double 的精度玄学,这是交易系统的基本功。spec_attrs 存的是可读的规格描述,列表页直接展示,不需要额外联表查规格字典;真正判断“用户选了哪几个规格”时,由小程序端拼装 key,后面第四章讲。
2.3 订单主表、订单明细与支付流水:状态机先定死
订单表是商城核心,支付回调、分账、售后都围着它转。状态字段用 tinyint,代码里用枚举映射,不要用字符串散落各处。订单主表存店铺 ID、买家 ID、总金额、实付、平台抽佣和两个关键状态:支付状态、分账状态。
create table `order` ( id bigint unsigned auto_increment primary key, order_no varchar(32) not null comment '业务订单号', shop_id bigint unsigned not null, member_id bigint unsigned not null, total_amount integer not null comment '总金额(分)', pay_amount integer not null comment '实付金额(分)', commission_amount integer not null default 0 comment '平台抽佣(分)', status tinyint not null default 0 comment '0待付款 1已付款 2已发货 3已完成 4已取消 5售后中', pay_status tinyint not null default 0 comment '0未支付 1已支付 2已退款', settle_status tinyint not null default 0 comment '0未分账 1已分账', pay_at datetime default null, created_at datetime not null default current_timestamp, unique key uk_order_no(order_no), key idx_shop_status(shop_id, status) ) engine=InnoDB comment='订单主表'; create table order_item ( id bigint unsigned auto_increment primary key, order_id bigint unsigned not null, sku_id bigint unsigned not null, sku_snapshot varchar(255) not null comment 'SKU快照JSON:名称、规格、图片、价格', price integer not null comment '成交单价(分)', quantity integer not null, total_amount integer not null, key idx_order(order_id) ) engine=InnoDB comment='订单明细表';订单明细里的 sku_snapshot 是快照字段,把下单那一刻的 SKU 名称、规格、图片、价格存成 JSON。商品可能改价、下架、删图,订单永远要显示顾客买到的原样。这里犯懒的话,售后对账时全是坑。
订单状态与支付、分账的关系建议按下表约定,字段之间要能互相解释:
| status 值 | 含义 | pay_status | settle_status |
|---|---|---|---|
| 0 | 待付款 | 0 未支付 | 0 未分账 |
| 1 | 已付款 | 1 已支付 | 0 未分账 |
| 2 | 已发货 | 1 已支付 | 0 未分账 |
| 3 | 已完成 | 1 已支付 | 1 已分账 |
| 4 | 已取消 | 0 未支付 | 0 未分账 |
| 5 | 售后中 | 1 或 2 | 0 或 1 |
settle_status 单独放订单表而不是和支付状态混在一起,是因为支付回调只负责改 pay_status,分账任务按 settle_status 轮询,两者解耦。后台运营改订单时也不会一手动错两个状态。
3. 在 NetCore 里把多商户 API 做成商用:JWT 鉴权、过滤器与分账
表结构定好后,NetCore 这层要解决三件事:登录后怎么知道用户属于哪个店、查询时怎么保证不串店、支付回调怎么做幂等和分账。顺序不能乱,先解决身份,再解决数据权限,最后处理钱。
3.1 JWT 里带上 ShopId,一次登录换来店铺上下文
小程序端登录后拿 code,后端拿 code 换 openid,再签发自己的 JWT。为什么不用微信的 session_key 当登录态?它是对称密钥,表达不了“用户属于哪个店、是店主还是店员”。自签 JWT 的好处是后面每个接口都能直接从令牌里取身份和店铺。
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { var secret = builder.Configuration["Jwt:Secret"] ?? throw new InvalidOperationException("Jwt:Secret 未配置"); options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(secret)), ValidateIssuer = false, ValidateAudience = false, ValidateLifetime = true, ClockSkew = TimeSpan.FromMinutes(2) }; }); builder.Services.AddAuthorization();Jwt:Secret 至少要 32 字节,生产环境用环境变量覆盖,不要写死在代码里。ClockSkew 给 2 分钟是为了容忍服务器时间偏差,太大会导致 Token 失效后还能继续访问。
签发 Token 时把店铺 ID 放 Claim:
var claims = new List<Claim> { new Claim(ClaimTypes.NameIdentifier, member.Id.ToString()), new Claim("shop_id", member.ShopId.ToString()), new Claim(ClaimTypes.Role, role) }; var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret)); var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( claims: claims, expires: DateTime.UtcNow.AddHours(2), signingCredentials: credentials); return new JwtSecurityTokenHandler().WriteToken(token);shop_id 放进 Token 后,接口里通过 User.FindFirst("shop_id") 取,不需要每次查数据库。这个设计后面所有店铺维度接口都依赖。这里也提一句 C# 里做 JSON 配置绑定的常见坑:前端 DTO 用 camelCase 命名,内部配置类用 PascalCase,System.Text.Json 序列化时命名策略要统一,否则上线后配置读出来全是 null。
3.2 用 .netcore 过滤器做租户隔离,而不是在每个方法里复制条件
多店铺 API 最容易犯的错是“谁都能查所有店铺数据”。NetCore 里最常用来兜底的是过滤器,把店铺上下文的解析统一放在过滤器里,业务层只从 HttpContext.Items 取当前店铺 ID,不在每个 Service 方法里拼Where(o => o.ShopId == xxx)。
public class TenantFilter : IAsyncActionFilter { public async Task OnActionExecutionAsync( ActionExecutingContext context, ActionExecutionDelegate next) { var shopIdClaim = context.HttpContext.User.FindFirst("shop_id")?.Value; if (!string.IsNullOrEmpty(shopIdClaim) && long.TryParse(shopIdClaim, out var shopId)) { context.HttpContext.Items["ShopId"] = shopId; } await next(); } }过滤器在模型绑定之前执行,它在整个请求管线的位置很靠前,后续 Controller 和 Service 统一取 Items["ShopId"]。这是 .netcore 过滤器最值钱的落地位置,很多人只拿过滤器写日志和异常处理,忽略了租户隔离。
Service 层取店铺 ID 的辅助类:
public class OrderService { private readonly long _shopId; private readonly AppDbContext _db; public OrderService(IHttpContextAccessor accessor, AppDbContext db) { _shopId = Convert.ToInt64( accessor.HttpContext?.Items["ShopId"] ?? 0); _db = db; } public async Task<List<Order>> GetOrderPage(int page, int pageSize) { return await _db.Orders .Where(o => o.ShopId == _shopId) .OrderByDescending(o => o.Id) .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(); } }即便是这样,新人还是可能在某个查询里忘加 _shopId。商用系统要再加一道防线:EF Core 全局查询过滤器,在 ModelBuilder 里统一配置。
modelBuilder.Entity<Order>() .HasQueryFilter(o => o.ShopId == _currentShopId);_currentShopId 从注入的 IHttpContextAccessor 解析。全局过滤器生效后,所有查询、更新、删除都会自动带店铺条件,就算业务层漏写了,数据库层面也会拦住,这就是常说的“最后一道防线”。
3.3 支付回调:验签、幂等、分账一次做完
微信支付回调会重试多次,处理逻辑必须幂等。顺序依次是:验签、按订单号查单、状态判断、更新订单、计算佣金、写入账户流水、同事务提交。
public async Task HandlePayCallback(PayNotifyDto dto) { var ok = WeChatPayHelper.VerifySign(dto); if (!ok) { throw new InvalidOperationException("支付回调验签失败"); } var order = await _db.Orders .FirstOrDefaultAsync(o => o.OrderNo == dto.OutTradeNo); if (order == null || order.PayStatus != PayStatus.Unpaid) { return; } using var tx = await _db.Database.BeginTransactionAsync(); order.PayStatus = PayStatus.Paid; order.PayAt = DateTime.Now; order.CommissionAmount = (int)Math.Round( order.PayAmount * order.CommissionRate, MidpointRounding.AwayFromZero); await _db.AccountFlows.AddAsync(new AccountFlow { ShopId = order.ShopId, OrderNo = order.OrderNo, Amount = order.PayAmount - order.CommissionAmount, FlowType = "SALE" }); await _db.SaveChangesAsync(); await tx.CommitAsync(); }回调最怕重复入账。PayStatus 判断是天然幂等,已经支付的订单直接返回成功,微信就不会再重试。分账用“订单状态 + 账户流水”在同一个事务里提交,要么都成功要么都回滚。CommissionRate 用的是订单表里的快照值,不是实时查 shop 表,否则平台改抽佣比例后历史订单全错。
3.4 列表加载更多:分页接口的返回结构要稳定
小程序端“加载更多”不复杂,后端把约定做稳就行。入参 page、pageSize,返回 records、total、hasMore,前端不用自己猜还有没有下一页。
public class PagedResult<T> { public List<T> Records { get; set; } public int Total { get; set; } public int Page { get; set; } public int PageSize { get; set; } public bool HasMore => Page * PageSize < Total; }HasMore 由后端计算,前端拿到 false 就停止请求。Page 和 PageSize 原样返回,调试时出了问题一眼能看到当前请求的参数。订单量超过十万行的店铺,建议换游标分页,条件用where shop_id = @shopId and id < @lastId order by id desc limit @size,性能稳定不深翻页。
4. 微信小程序端的三件硬事:登录态、SKU 选择与支付回跳
小程序端是顾客直接接触的入口,功能可以不多,但登录、SKU、支付这三件事必须稳。下面代码都按原生小程序写法,不依赖第三方框架。
4.1 静默登录与 Token 缓存
小程序登录链路是:wx.login 拿临时 code,POST 给后端,后端用 code 换 openid 并签发 JWT,小程序把 token 存本地。关键坑在每个接口都可能触发登录,但 code 只能用一次。
let _loginPromise = null; function login() { if (_loginPromise) return _loginPromise; _loginPromise = new Promise((resolve, reject) => { wx.login({ success: (res) => { if (!res.code) { reject(new Error('获取code失败')); return; } wx.request({ url: `${app.globalData.baseUrl}/api/auth/wxlogin`, method: 'POST', data: { code: res.code }, success: (resp) => { if (resp.statusCode === 200) { wx.setStorageSync('token', resp.data.token); resolve(resp.data); } else { reject(new Error(resp.data.msg)); } }, fail: reject }); }, fail: reject }); }); _loginPromise.finally(() => { _loginPromise = null; }); return _loginPromise; }_loginPromise 是登录防重入锁,多个接口同时请求时只触发一次 wx.login。trust me,不这么写,线上就会偶尔出现 40029 或 40163,原因都是同一个 code 被后端消费了两次。token 存本地后,每个请求在 header 里带 Authorization: Bearer,后端过滤器会自动解析出 shop_id。
4.2 SKU 选择器:库存联动与下单参数
SKU 选择不一定要写递归算法,常见做法是预生成“规格组合到 SKU 的映射”,前端选中规格后直接查表。
const skuMap = {}; goods.skuList.forEach(sku => { const key = sku.spec_attrs .split(';') .map(item => item.split(':')[1]) .join('#'); skuMap[key] = sku; }); function getSkuBySpecs(selected) { const key = ['颜色', '尺码'] .map(name => selected[name] || '') .join('#'); const sku = skuMap[key]; return sku && sku.stock > 0 ? sku : null; }spec_attrs 如果存的是“颜色:黑色;尺码:M”,映射 key 就是“黑色#M”。用户每点一个规格,重新调 getSkuBySpecs,命中就显示价格和库存,没命中或库存不足就把按钮置灰。SKU 数量再多,核心逻辑也是这张映射表,不用过度设计。
4.3 统一下单与 wx.requestPayment
调起支付前,前端先请求后端统一下单接口,后端返回签名后的参数,前端直接传给 wx.requestPayment。
async function pay(orderNo) { const resp = await request('/api/pay/unified', { orderNo }); if (!resp || resp.code !== 0) { throw new Error(resp.msg); } wx.requestPayment({ timeStamp: resp.data.timeStamp, nonceStr: resp.data.nonceStr, package: resp.data.package, // 字段名必须叫 package,不能改名 signType: 'RSA', paySign: resp.data.paySign, success: () => { wx.redirectTo({ url: `/pages/order/detail?orderNo=${orderNo}` }); }, fail: (e) => { wx.showToast({ title: '支付未完成,可在订单列表继续支付', icon: 'none' }); } }); }timeStamp 必须传字符串,后端返回数字会在低版本微信上直接调不起支付。package 是保留字段名,改成别的 key 支付也会失败。还有一条容易被忽略:wx.requestPayment 的 fail 不代表订单支付失败,必须以“后端支付回调是否收到”为准。顾客可能付完钱刚好断网,前端收到 fail 但钱已经扣了,这种情况要以订单详情接口的状态为准。
4.4 列表加载更多 + 顶部导航栏高度适配
页面列表加载更多是高频搜索词,实现不复杂,核心在防重入和 hasMore 判断。
Page({ data: { list: [], page: 1, hasMore: true, loading: false }, onLoad() { this.loadMore(); const rect = wx.getMenuButtonBoundingClientRect(); const system = wx.getSystemInfoSync(); this.setData({ navTop: rect.top, navHeight: rect.height, statusBarHeight: system.statusBarHeight }); }, onReachBottom() { if (this.data.loading || !this.data.hasMore) return; this.loadMore(); }, async loadMore() { this.setData({ loading: true }); const resp = await request('/api/orders', { page: this.data.page, pageSize: 10 }); this.setData({ list: this.data.list.concat(resp.records), page: this.data.page + 1, hasMore: resp.hasMore, loading: false }); } });onReachBottom 触发加载,loading 是防重入锁,避免快速滚动时重复请求。hasMore 用后端返回的字段,不用前端猜。自定义导航栏时,顶部导航栏高度用 wx.getMenuButtonBoundingClientRect 拿胶囊位置,再结合 statusBarHeight 算出占位高度,不然 iPhone 刘海屏上标题栏会顶到状态栏。
5. 多店铺商城常见问题与排查:登录 40029、Body 流与超卖
这一章是血泪经验汇总,每一条都是真实会踩的坑。按“现象、原因、解决”三步写,排查时照着顺序走。
5.1 过滤器读完 Body,控制器拿到空请求体
现象:加了支付回调验签过滤器后,控制器方法接收到的参数全是 null,JSON 绑定失败。
原因:Request.Body 是单向流,第一次读取后没有重置 Position,后续模型绑定读不到内容。
解决:读取前调用 EnableBuffering,读完后把 Position 归零。
context.HttpContext.Request.EnableBuffering(); using (var reader = new StreamReader( context.HttpContext.Request.Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: 1024, leaveOpen: true)) { body = await reader.ReadToEndAsync(); } context.HttpContext.Request.Body.Position = 0;EnableBuffering 让请求流支持重复读,Position = 0 是让接下来的模型绑定从头读。leaveOpen: true 保证流不被释放。这个坑几乎每个自研支付回调的小组都会踩一次,想起来就头疼。
5.2 code2session 的 40029 / 40163:重复使用 code
现象:小程序登录偶发失败,后端调微信接口返回 errcode 40029 或 40163。
原因:wx.login 的 code 是一次性的,前端多个接口同时触发登录,或者用户快速退出重进,同一 code 被后端请求了两次。
解决:前端用单例 Promise 包装登录,后端再对 code 做消费记录。这问题最像玄学,因为它不是必现,而是并发时才出现。排查时先看后端日志里有没有同一个 code 出现两次,有就是前端去重没做好。
5.3 店铺串数据:店主看到了别家店的订单
现象:A 店运营反馈,后台订单列表里出现了 B 店的订单。
原因:列表查询条件漏了 shop_id,新的 Service 方法没走统一过滤。
解决:除了过滤器,再加 EF Core 全局查询过滤器兜底:
modelBuilder.Entity<Order>() .HasQueryFilter(o => o.ShopId == _currentShopId);_currentShopId 从 IHttpContextAccessor 解析。全局过滤器对查询、更新、删除全部生效,就算业务层漏掉条件也查不到别家数据。DbContext 生命周期是 Scoped,构造函数里注入 IHttpContextAccessor 即可,注意别用静态服务定位器,不然高并发下会取到别人的店铺。
5.4 layui table 第二页没有数据
现象:管理后台用 layui table 分页,第一页 10 条正常,切到第二页为空。
原因:layui 默认传 page 和 limit,后端接收参数名可能是 pageNo、pageSize,参数映射不上。
解决:统一后端分页参数名为 page 和 pageSize,Controller 直接[FromQuery] int page, [FromQuery] int pageSize;如果后端接口不能改,就在 table.render 里重映射:
table.render({ url: '/api/orders', request: { pageName: 'page', limitName: 'pageSize' } });layui 分页还有一个隐性坑:返回字段默认要叫 count 和 data,和后端 PagedResult 的 total、records 对不上。最简单的办法是 table.render 里加 parseData 做转换,或者后端直接返回 layui 约定的结构。
5.5 并发下单击穿库存:把库存写成负数
现象:秒杀活动时 SKU 库存变成 -2、-5。
原因:代码是“先查库存,判断够不够,再 UPDATE”。两个请求同时通过检查,同时执行扣减,库存就穿底了。
解决:把库存扣减写成一条带条件的原子 UPDATE,靠影响行数判断成败。
var rows = await _db.Database.ExecuteSqlRawAsync( "update sku set stock = stock - {0}, sale_count = sale_count + 1 " + "where id = {1} and stock >= {0}", quantity, skuId); if (rows == 0) { return Fail("库存不足"); }rows == 0 就说明没有扣减成功,直接返回库存不足。这个方式在本地事务里就解决了超卖,不需要上分布式锁和消息队列,对自研商城来说是最划算的方案。分布式锁不是不能用,而是引入前先确认数据库约束真的兜不住。
6. 上线前值得做的三件小事:压测口径、日志定位与体验版试运营
商用标准不是代码写得漂亮,而是出问题时能快速定位、钱和货不出差错。上线前这三件事建议安排进迭代计划。
第一,压测先盯“写链路”。固定“登录 - 商品列表 - 下单 - 支付回调 - 分账”这条链路,用并发工具从 10、50、100 逐级加压。重点观察两个数据:库存有没有变负数,支付回调重复通知后流水有没有重复。加库存并发和支付幂等过了,比首页扛一万并发有意义得多。
第二,日志要能按订单号串起来。支付回调对很多团队是个黑匣子,建议用 Serilog 做结构化日志,把 ShopId、MemberId、OrderNo 放进上下文字段:
using var logScope = LogContext.PushProperty("OrderNo", orderNo); Log.Information("开始处理支付回调");任何一笔支付问题,按 order_no 就能把“小程序请求 → 后端接口 → 微信回调 → 分账流水”整条链路拉出来。我自己的习惯是支付和分账相关日志单独写一个文件,排查时不用在业务日志里大海捞针。
第三,正式上线前一定要发体验版试运营。开发者工具上传代码后,在微信公众平台里把当前版本设为体验版,生成体验版二维码,把店主和运营人员加为体验成员,让他们真机下单测试三五天。收集试用反馈时注意两件事:业务域名和 request 合法域名必须提前配好,不然真机上一半请求直接失败;H5 里要跳小程序的,必须用微信开放标签并做域名校验,这些都是上线当天没法临时补救的东西。
最后分享一个缓存设计的后悔药。多店铺缓存键必须带 shop_id 前缀:
var cacheKey = $"shop:{shopId}:goods:{spuId}";不加店铺前缀的缓存一定会串店,加了之后不同店铺的商品天然隔离。过期时间加随机抖动,避免整点大批缓存同时失效打崩数据库。我之前吃过亏的地方,是只给订单表加了全局过滤器、忘了给售后表也加上,结果售后单串店排查了一下午。后来定了条规矩:所有新表落地前先回答一个问题——这张表带不带 shop_id,不带就是平台级表。希望帮到你。
本文还有配套的精品资源,点击获取