从单商户到多店铺:NetCore+微信小程序商城架构设计与避坑实践

发布时间:2026/10/30 15:47:48
从单商户到多店铺:NetCore+微信小程序商城架构设计与避坑实践 简介这是一套基于微信小程序与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 ) engineInnoDB 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) ) engineInnoDB 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) ) engineInnoDB 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) ) engineInnoDB commentSKU表;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) ) engineInnoDB 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) ) engineInnoDB comment订单明细表;订单明细里的 sku_snapshot 是快照字段把下单那一刻的 SKU 名称、规格、图片、价格存成 JSON。商品可能改价、下架、删图订单永远要显示顾客买到的原样。这里犯懒的话售后对账时全是坑。订单状态与支付、分账的关系建议按下表约定字段之间要能互相解释status 值含义pay_statussettle_status0待付款0 未支付0 未分账1已付款1 已支付0 未分账2已发货1 已支付0 未分账3已完成1 已支付1 已分账4已取消0 未支付0 未分账5售后中1 或 20 或 1settle_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 放 Claimvar claims new ListClaim { 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 命名内部配置类用 PascalCaseSystem.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 TaskListOrder 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.EntityOrder() .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 PagedResultT { public ListT 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 拿临时 codePOST 给后端后端用 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控制器拿到空请求体现象加了支付回调验签过滤器后控制器方法接收到的参数全是 nullJSON 绑定失败。原因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.EntityOrder() .HasQueryFilter(o o.ShopId _currentShopId);_currentShopId 从 IHttpContextAccessor 解析。全局过滤器对查询、更新、删除全部生效就算业务层漏掉条件也查不到别家数据。DbContext 生命周期是 Scoped构造函数里注入 IHttpContextAccessor 即可注意别用静态服务定位器不然高并发下会取到别人的店铺。5.4 layui table 第二页没有数据现象管理后台用 layui table 分页第一页 10 条正常切到第二页为空。原因layui 默认传 page 和 limit后端接收参数名可能是 pageNo、pageSize参数映射不上。解决统一后端分页参数名为 page 和 pageSizeController 直接[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不带就是平台级表。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询