ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于ASP.NET Core 8.0的家具管理系统开发实战

基于ASP.NET Core 8.0的家具管理系统开发实战 简介这是一套面向.NET开发者与电商项目团队的现代化家具管理系统后端源码基于ASP.NET Core 8.0构建采用清洁架构设计完整覆盖用户端商品浏览、购物车、下单及管理员侧库存、用户、促销等核心业务场景特别适合学习Web API开发、微服务通信模式与第三方支付集成的中高级开发者。资源包共245个文件含207个C#业务与实体类如AppDbContextModelSnapshot、EmailTemplateService、多版EF迁移脚本、4个csproj工程文件、21个SVG图标资源、6个JSON配置与模板文件以及Dockerfile、YAML部署配置、License与README等关键支撑文件整体仅326KB结构精炼、模块边界清晰。已有72人下载学习可直接运行调试快速掌握JWT/OAuth认证、Stripe支付对接、AI驱动的智能搜索、实时通知与评分机制等实战能力并深入理解领域层与基础设施层解耦的设计实践。1. 项目背景与技术选型为什么要用ASP.NET Core 8.0做家具管理系统1.1 家具行业的业务痛点与系统价值今年接了一个家具工厂的信息化项目老板上来就摆了一堆实际问题产品型号几百个面料、板材、五金件各有各的供应商库存账总是对不上销售订单靠Excel来回传生产排期全凭老员工记忆。聊完发现这根本不是一个“做个网站”的需求而是一整套围绕产品、库存、订单、采购的流程再造。市面上现成的ERP要么太重、定制成本高要么太轻、管不了家具行业特有的SKU维度。最后决定自己搭一套技术栈就锁定了ASP.NET Core 8.0。选ASP.NET Core 8.0不是拍脑袋。一是它跨平台客户那边既有Windows Server也有Linux服务器部署灵活二是性能比Framework时代提升了好几个量级应对几百人同时操作的内部系统绰绰有余三是生态成熟EF Core、JWT认证、Swagger文档、Docker部署这些配套工具链齐全开发效率有保证。最关键的一点.NET 8是LTS版本微软提供三年长期支持对传统制造企业来说稳定性和维护周期比追新更重要。这个系统最终要解决的是家具行业三个核心问题产品管理混乱同款不同配置算多个SKU还是合并管理、库存账实不符原材料、半成品、成品三种形态怎么联动、订单履约链路长从下单到生产到发货状态怎么透明化。围绕这三个痛点系统划分成商品中心、库存中心、订单中心、采购中心、系统管理五大模块每个模块内部再细分子功能。1.2 技术选型思路框架选型的底层逻辑项目启动前团队内部其实争论过一阵子技术方案。有人提议用Node.js NestJS有人想上Spring Boot还有人建议直接用低代码平台。我坚持用ASP.NET Core 8.0核心理由有三条。第一团队技术栈匹配度。小组成员之前都是写C#的从.NET Framework 4.8迁移到.NET Core 8的学习成本最低。如果强行切到Java或Node光熟悉生态就要一两周项目交付压力不允许。技术选型不是选“最好的技术”而是选“最合适的组合”。第二EF Core的数据建模能力。家具管理系统的数据模型比一般进销存复杂得多。一款沙发可能有不同面料、不同尺寸、不同颜色这些属性组合起来才是一个可售的SKU。传统做法是搞一大堆关联表EF Core的Fluent API和导航属性可以很优雅地表达这种多级嵌套关系写起来顺手维护起来也清晰。第三部署和运维的便利性。ASP.NET Core应用可以打成单文件发布直接丢到服务器上跑不依赖服务器是否装了运行时。配合Docker容器化一套docker-compose就能把应用、数据库、Redis全部编排起来客户那边的IT人员不用学太多新东西就能维护。提示.NET 8是LTS版本支持到2026年11月10日。如果你正在规划企业级项目建议直接选LTS版本不要选STS标准期限支持版本比如.NET 7或.NET 9避免两年后被迫升级。2. 系统整体架构与数据模型设计2.1 分层架构与项目结构系统采用经典的分层架构从上到下依次是表现层Web API、应用层Application Service、领域层Domain、基础设施层Infrastructure。用一句话解释就是表现层只负责接收请求和返回结果应用层负责业务逻辑编排领域层放核心业务规则基础设施层管数据库、文件存储、缓存这些外部依赖。实际解决方案里我建了四个独立项目职责边界非常清晰FurnitureManage.sln ├── src/ │ ├── FurnitureManage.Api # 表现层Controller、Filter、Middleware │ ├── FurnitureManage.Application # 应用层DTO、服务接口与实现、校验 │ ├── FurnitureManage.Domain # 领域层实体、值对象、仓储接口、领域服务 │ └── FurnitureManage.Infrastructure # 基础设施层EF Core DbContext、仓储实现、Redis、文件存储 └── tests/ └── FurnitureManage.UnitTests # 单元测试这种分层在团队协作时的优势特别明显。UI工程师只负责Api层和前端对接业务开发专注Application层的服务实现数据层的同事只需要盯着Infrastructure。互不干扰代码冲突都少了很多。有人可能会说一个小型管理系统搞这么多层是不是过度设计我的观点是——取决于项目生命周期。家具制造企业的管理系统往往要用五到十年前期多花两天搭好架子后期加功能、改需求时会省很多事。比如客户后来要求对接ERP我们直接在Application层加了一个适配服务领域层和表现层一点没动。2.2 核心业务数据模型设计数据模型是整个系统的地基设计阶段花费的精力最多。家具行业SKU管理有个显著特点产品属性维度多且不固定。一张餐桌可能有“材质、尺寸、颜色、风格”四个维度一套定制衣柜可能还要加“门板类型、五金品牌、封边工艺”。用传统固定字段的方式建模改一次需求就要改一次表结构根本经不起折腾。我采用的是“SPU SKU”模型在电商领域已经很成熟了。SPUStandard Product Unit代表一个产品比如“北欧风实木餐桌”SKUStock Keeping Unit代表一个具体的可售规格比如“北欧风实木餐桌-橡木-1.4米-原木色”。产品表存公共属性SKU表存差异化属性属性值用JSON格式存储。EF Core 8对JSON列映射的支持已经比较成熟查询和序列化都很方便。订单这块核心是订单主表 订单明细表的经典设计。订单主表记录客户信息、订单状态、总金额、收货地址明细表记录每一个SKU的购买数量、单价、小计。需要注意的一个细节是快照设计也就是说订单明细里要冗余SKU的名称、图片、规格参数这些信息。为什么因为用户下单之后后台可能修改了SKU名称如果订单只存SKU的Id历史订单显示的名称就会跟着变这在财务对账时会造成麻烦。库存模型比较特殊我拆成了两个维度可用库存Available Stock和锁定库存Locked Stock。用户下单时先锁定库存付款后再真正扣减。这样做的好处是避免超卖。之前用过直接把库存字段减一的做法结果遇到取消订单回补逻辑没写好库存对不上账。改成可用库存和锁定库存两个字段之后配合事务处理账目一致性有了保障。3. 核心功能模块的落地实现3.1 商品管理模块SKU设计与属性规格处理商品管理模块是整个系统的门面也是最容易做砸的地方。前前后后改了四版最终沉淀出来的方案是三个表Category分类、Product产品、ProductSku规格。分类表用自引用实现树形结构ParentId指向自身的Id支持无限层级。比如“客厅家具 - 沙发 - 布艺沙发”就是三层。查询某个分类下的所有商品时只需要递归取出所有子分类Id再拼接查询条件。数据量不大时用递归或者直接加载后内存处理都行。产品表的核心字段包括编号、名称、品牌Id、分类Id、状态、主图地址、详情描述。SKU表的字段包括产品Id、SKU编码、规格属性值JSON、销售价、成本价、条形码、状态、图片地址。这里最关键的设计决策是产品的通用属性放Product表差异化属性放Sku表的JSON字段里。为什么不全用关系表存属性因为家具的属性维度是动态变化的一个卖椅子的可能只需要“材质、颜色”一个卖床垫的要“尺寸、软硬度、厚度、面料”。如果用EAVEntity-Attribute-Value模式属性查询要关联好几张表性能差且代码复杂。JSON字段配合MySQL 8.0的JSON查询函数可以直接在数据库层面对属性做筛选简单直接。实际开发中还发现前后端对属性数据的处理方式要统一约定。前端提交SKU时属性以JSON对象形式上传后端接收后做格式校验并存储查询时后端把属性JSON直接透传给前端展示。整个过程保持“后端不解析业务属性只负责存储和透传”的原则这样即使以后加新的属性维度后端代码一行都不用改。提示给每个SKU生成唯一编码时建议用“产品编号 规格序号”的组合方式比如PT001-S01。不要用自增Id当编码自增Id在数据迁移、多库合并时会出问题。条形码Barcode字段建议独立存储可以对接后续的PDA扫码出入库功能。3.2 库存中心出入库流水与库存账一致性库存模块是整个系统里最容易出问题的部分因为家具行业的库存不是单层的原材料、半成品、成品是三种不同的库存形态。比如一张实木餐桌需要桌面板原材料、桌腿原材料、组装好的餐桌成品这三者之间的转换关系需要靠数据来追踪。最终实现上我设计了三张核心表Inventory库存快照表、StockTransaction库存流水表、StockCheck盘点单表。库存快照表存的是某个仓库、某个SKU的当前可用库存和锁定库存。库存流水表记录每一次库存变动包括入库、出库、锁定、解锁、盘盈、盘亏每条流水都有业务单号关联。盘点单表用于月度/季度盘点支持盘点差异自动生成调整流水。为什么一定要有流水表我举个实际场景销售反馈某款沙发库存数据不准客服查了说是进了50件但系统只显示30件。没有流水表的话排查起来只能翻Excel手工对账有了流水表按SKU和时间段一查每一笔变动都清清楚楚是谁在哪个时间点做了什么操作一目了然。流水表设计时加了一个关键字段BusinessType业务类型和BusinessNo业务单号。业务类型包括采购入库、销售出库、销售退货、采购退货、盘点调整、初始化导入等。每个业务类型关联对应的业务单据比如销售出库会关联订单号采购入库会关联采购单号。这样库存问题可以快速追溯到源头单据。库存扣减和回补的事务一致性我用的是数据库事务 乐观锁双保险。EF Core的并发令牌特性可以在Update时带上Version字段作为乐观锁多个用户同时操作同一SKU库存时只有一个能成功更新其余会抛出并发冲突异常再由程序自动重试。public async Taskbool LockStockAsync(int skuId, int quantity) { // 乐观锁重试机制并发冲突时最多重试3次 for (var retry 0; retry 3; retry) { await using var transaction await _dbContext.Database.BeginTransactionAsync(); try { var inventory await _dbContext.Inventories .FirstOrDefaultAsync(x x.SkuId skuId); if (inventory null || inventory.AvailableStock quantity) return false; inventory.AvailableStock - quantity; inventory.LockedStock quantity; inventory.Version; // 乐观锁版本号 _dbContext.StockTransactions.Add(new StockTransaction { SkuId skuId, ChangeType StockChangeType.Lock, ChangeQuantity quantity, BusinessType BusinessType.SalesOrder, BusinessNo orderNo, CreatedAt DateTime.Now, Remark $订单 {orderNo} 锁定库存 }); await _dbContext.SaveChangesAsync(); await transaction.CommitAsync(); return true; } catch (DbUpdateConcurrencyException) { await transaction.RollbackAsync(); // 重试逻辑 } } throw new InvalidOperationException(库存操作多次冲突请稍后重试); }这段代码在生产环境跑了几个月并发冲突抛异常的概率很低但防患于未然还是值得的。库存这个领域宁可代码多写几行也不能让数据错。3.3 订单中心全流程状态机与事务处理订单模块的复杂度在于状态流转。一个订单从创建到完成要经历待付款 - 待发货 - 已发货 - 已完成中间还有取消、退款、异常这几个分支状态。如果各个状态之间的跳转逻辑分散在代码里各个地方时间一长就会乱套。我用的方案是显式的状态机模式把订单状态定义成枚举用一个静态类维护合法的状态流转表。任何状态跳转都要先经过合法性校验。订单状态流转的关键实现public static class OrderStateMachine { // 合法状态流转字典当前状态 - 允许跳转的目标状态集合 private static readonly DictionaryOrderStatus, OrderStatus[] AllowedTransitions new() { [OrderStatus.PendingPayment] new[] { OrderStatus.Paid, OrderStatus.Cancelled }, [OrderStatus.Paid] new[] { OrderStatus.Shipped, OrderStatus.Refunded, OrderStatus.Cancelled }, [OrderStatus.Shipped] new[] { OrderStatus.Completed, OrderStatus.Refunded }, [OrderStatus.Completed] new[] { OrderStatus.Refunded }, [OrderStatus.Cancelled] new[] { OrderStatus.PendingPayment }, // 特殊情况取消后重新支付 [OrderStatus.Refunded] Array.EmptyOrderStatus() }; public static bool CanTransition(OrderStatus current, OrderStatus target) { return AllowedTransitions.TryGetValue(current, out var targets) targets.Contains(target); } }这个状态机的设计带来的好处是门卫逻辑收敛在了一处任何Service方法里要跳转状态时先调用CanTransition校验如果非法就抛出业务异常。即使以后新增一个状态比如“部分发货”只需要改动这一处定义其他代码不受影响。订单创建流程本身也是一个事务性的操作校验购物车、生成订单号、锁定库存、清空购物车、返回支付参数。这些操作如果分散在多个方法里很容易出现“订单建了但库存没锁住”的中间状态。我统一用应用层的一个方法编排整体包在一个事务里执行任何一步失败全部回滚。订单号生成也值得聊一下。之前用的是简单的Guid但是客户反馈说订单号太难念、不方便在电话里报给客服。后来改成了“yyyyMMdd 5位当天序号”的形式比如2024011500032既方便按天统计订单量客服电话沟通时也容易读。3.4 供应商与采购管理补货提议功能采购模块看起来简单就是记录供应商、采购单、入库单但真正做好并不容易。家具厂采购有一个特点原材料品类多、供应商杂、交货周期不同。同一个板材可能有两家供应商价格和交货期都不一样采购员需要根据当前库存情况快速决定找谁下单。系统里做了一个“补货提议”功能每天定时任务扫描所有原材料的库存状态当某个SKU的可用库存低于设定的安全库存阈值时自动生成一条补货建议包含当前库存量、日均消耗量、预估可售天数、建议采购量。采购员根据补货建议生成采购单再分发给对应供应商。这个功能的实现用了一个简单的公式建议采购量 (安全库存 - 当前可用库存) 预计消耗量。预计消耗量是基于过去30天的日均出库量乘以采购提前期计算的。公式不复杂但实际使用下来效果很好采购员反馈说再也不用天天盯着库存表看了。采购入库的流程和数据流设计也花了不少心思。采购单创建后状态是“待审核”审核通过后变成“待发货”供应商发货后改成“已发货”到货验收入库后变成“已完成”。每一步都有操作日志和操作人记录方便后续追溯。收到货后仓库管理员在系统里点击“入库确认”系统自动增加对应SKU的可用库存同时生成采购入库流水。4. 实操过程从零搭建项目的关键步骤4.1 环境准备与项目创建如果你也想从零搭一个类似的管理系统下面的步骤可以直接参考。假设你已经在机器上安装了.NET SDK 8.0和Visual Studio 2022版本建议17.8以上。第一步创建解决方案和项目。使用dotnet CLI是最快的方式命令如下dotnet new sln -n FurnitureManage dotnet new webapi -n FurnitureManage.Api -o src/FurnitureManage.Api dotnet new classlib -n FurnitureManage.Application -o src/FurnitureManage.Application dotnet new classlib -n FurnitureManage.Domain -o src/FurnitureManage.Domain dotnet new classlib -n FurnitureManage.Infrastructure -o src/FurnitureManage.Infrastructure dotnet sln add src/FurnitureManage.Api src/FurnitureManage.Application src/FurnitureManage.Domain src/FurnitureManage.Infrastructure第二步配置项目引用关系。引用方向遵循架构分层从上往下引用dotnet add src/FurnitureManage.Api reference src/FurnitureManage.Application dotnet add src/FurnitureManage.Application reference src/FurnitureManage.Domain dotnet add src/FurnitureManage.Infrastructure reference src/FurnitureManage.Domain dotnet add src/FurnitureManage.Api reference src/FurnitureManage.Infrastructure这里有一个设计细节Application层只依赖Domain层不直接依赖Infrastructure层。依赖倒置原则的应用让Application层的业务逻辑不关心具体的数据存储实现。测试时可以方便地替换成内存数据库。第三步引入NuGet包。我用的核心包如下项目NuGet包用途FurnitureManage.ApiSwashbuckle.AspNetCore生成Swagger文档FurnitureManage.ApiMicrosoft.AspNetCore.Authentication.JwtBearerJWT认证FurnitureManage.InfrastructureMicrosoft.EntityFrameworkCoreEF Core核心FurnitureManage.InfrastructurePomelo.EntityFrameworkCore.MySqlMySQL驱动FurnitureManage.InfrastructureStackExchange.RedisRedis缓存FurnitureManage.InfrastructureAutoMapper.Extensions.Microsoft.DependencyInjectionDTO映射数据库用的是MySQL 8.0。选MySQL而不是SQL Server主要考虑两方面一是客户现有的服务器是Linux环境MySQL部署更自然二是从成本角度看MySQL没有许可证费用对预算有限的制造企业更友好。4.2 核心初始化配置CORS、JWT认证与统一响应封装系统上线后要同时给PC端后台和移动端H5用所以跨域问题必须提前配置好。CORS跨域资源共享配置上我限制了允许的源、方法和请求头而不是图省事直接AllowAnyOrigin。生产环境这么做风险很大别学那些随便放开的写法。builder.Services.AddCors(options { options.AddPolicy(CorsPolicy, policy { var allowedOrigins builder.Configuration.GetSection(Cors:AllowedOrigins).Getstring[]() ?? new[] { http://localhost:5173 }; policy.WithOrigins(allowedOrigins) .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials(); }); });JWT认证配置是每一个Web API项目都绕不开的环节。我用的是JwtBearer中间件配合自定义的Token生成服务。密钥存放在配置文件里长度至少32位生产环境建议用环境变量或密钥管理服务注入不要硬编码在代码里。builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options { options.TokenValidationParameters new TokenValidationParameters { ValidateIssuer true, ValidIssuer builder.Configuration[Jwt:Issuer], ValidateAudience true, ValidAudience builder.Configuration[Jwt:Audience], ValidateLifetime true, ClockSkew TimeSpan.FromMinutes(1), ValidateIssuerSigningKey true, IssuerSigningKey new SymmetricSecurityKey( Encoding.UTF8.GetBytes(builder.Configuration[Jwt:Key])) }; });统一响应格式也是企业级API必不可少的一环。前端开发最讨厌后端返回格式五花八门一会儿是JsonResult一会儿是裸对象一会儿是字符串。我做了一个统一的ApiResponse包装类所有Controller的返回值都用它包一层结构固定为{ code: 200, message: success, data: {} }这个格式约定让前后端联调效率提升明显。前端只要写一个axios拦截器统一处理code不为200的情况即可不用每个页面单独写错误处理。4.3 EF Core数据迁移与种子数据EF Core的数据迁移采用Code-First模式。先定义实体类再用迁移命令生成数据库表结构。开发过程中的操作顺序是修改实体 - 添加迁移 - 更新数据库。dotnet ef migrations add InitCreate dotnet ef database update这里踩过的坑是默认情况下dotnet ef命令找不到启动项目的DbContext。需要指定启动项目和输出目录。我最常用的命令是dotnet ef migrations add AddProductModule \ --project src/FurnitureManage.Infrastructure \ --startup-project src/FurnitureManage.Api \ --output-dir Migrations如果不指定--output-dir迁移文件会生成在Infrastructure项目根目录下时间长了文件一多就乱套了。种子数据我用了EF Core的HasData配置在OnModelCreating里为字典类型数据如角色、状态码预置初始值。但主业务数据的初始化不推荐用HasData硬编码因为后期维护起来非常痛苦。更推荐的做法是编写一个SeedDataService启动时自动检测数据库是否有数据没有的话通过业务接口写入。这样既能保证初始数据灵活性又不会在迁移历史里留下一堆垃圾记录。提示使用MySQL 8.0时EF Core的连接字符串要特别注意字符集设置。推荐在连接字符串中显式加上CharSetutf8mb4;否则存emoji或者某些特殊生僻字时会出现数据丢失或乱码。这算是一个老生常谈但是始终有人踩的坑。5. 踩坑记录与问题排查实录5.1 开发过程中的典型问题与解决方案把项目开发中遇到的高频问题整理成了一张速查表方便大家直接对照问题现象原因分析解决方案VS2022编译报错“using声明在C# 7.3中不可用请使用8.0或更高的语言版本”项目TargetFramework不是net8.0或者LangVersion被项目文件显式指定为低版本检查csproj里的 确认是net8.0。如果手工指定了LangVersion删除或用8.0Docker Compose启动MySQL容器失败日志显示chmod/cnofig错误MySQL 8.0容器初始化时权限不足或者挂载了不兼容的数据目录检查挂载目录权限给MySQL容器加command: --default-authentication-pluginmysql_native_password参数数据目录确保空目录Swagger页面打不开报window is not definedSwagger的JS资源加载失败通常是虚拟路径配置问题在Program.cs中配置app.UseSwaggerUI(c c.SwaggerEndpoint(/swagger/v1/swagger.json, FurnitureManage API v1))EF Core查询数据很慢只查几千条就超过3秒通常在循环中进行数据库查询产生了N1问题使用Include或ThenInclude预加载导航属性或改用Join查询一次取数JWT Token过期后前端跳转404前端没有在401响应时统一跳转登录页前端axios响应拦截器统一处理401状态N1问题单独展开说一下。有一次客户反馈商品列表页面打开很慢排查发现查询100个商品时EF Core默认懒加载每个商品的分类、品牌、SKU列表导致执行了1 N次SQL查询。100个商品就有101条SQL语句这种写法不慢才怪。修复方式是把查询改成显式加载var products await _dbContext.Products .Include(p p.Category) .Include(p p.Brand) .Include(p p.Skus) .Where(p p.Status ProductStatus.Enabled) .OrderByDescending(p p.CreatedAt) .Skip(pageIndex * pageSize) .Take(pageSize) .ToListAsync();5.2 部署阶段的两个关键细节系统部署用Docker Compose编排三个容器应用容器、MySQL容器、Redis容器。这里分享两个实际踩过的部署经验。第一个是关于MySQL 8.0镜像的默认认证插件问题。MySQL 8.0默认使用caching_sha2_password认证但一些老的数据库客户端工具不支持这个插件导致连接报错。解决方法是创建用户时指定mysql_native_password或者在docker-compose里加参数。顺带说一句不管用哪种方案请一定不要使用root账号跑应用连接单独创建应用账号并授权。第二个是关于ASP.NET Core应用的健康检查。我们给应用容器配置了healthcheck让Docker Compose可以自动发现应用是否启动成功。如果不加这个应用启动失败时编排工具不会报错流量依然会打过来然后出现502。配置方式是在docker-compose.yml里services: furniture-api: image: furniture-manage-api:latest ports: - 8080:8080 environment: - ConnectionStrings__DefaultServermysql;Port3306;Databasefurniture_db;Userapp_user;Passwordapp_password;CharSetutf8mb4; depends_on: mysql: condition: service_healthy restart: always这里的关键是condition: service_healthy它保证MySQL容器完全就绪后才启动应用容器。一开始我没配这个结果应用启动时数据库还没准备好EF Core初始化直接抛异常容器反复重启。加了健康检查之后就稳定了。6. 项目上线后的效果与扩展空间系统上线至今跑了快五个月业务侧反馈最明显的变化有三个。一是库存对账时间从原来的每个月花两天缩短到两个小时财务月底再也不用加班加点核对Excel了。二是订单处理效率明显提升从下单到仓库发货的响应时间缩短了40%左右。三是采购补货提议功能上线后缺货率下降了约30%采购员不再需要每天手工检查库存了。当然系统仍然有很多可以改进的地方。目前正在计划中的功能包括对接企业微信实现审批流消息通知、增加销售数据的多维度统计报表、引入条形码/PDA扫码出入库操作。这些功能都是基于当前的架构做增量开发不需要推翻重来这也是当初花时间做好分层设计带来的红利。最后再分享一个团队内部的实践经验做这类企业管理系统的过程中最耗精力的往往不是技术难题而是跟业务方确认需求细节的过程。技术方案再漂亮如果跟实际业务流程脱节落地的时候一定会被打回来重做。所以每开发完一个模块尽量让业务方实际操作用几天收集真实反馈后再迭代。这个习惯帮我们避了不少坑也省了很多重复开发的成本。本文还有配套的精品资源点击获取
返回列表