news 2026/9/9 19:57:29

Serverless Framework 自定义域名(Custom Domains)完整指南:基于 AWS API Gateway、ACM 与 Route 53 的自动化域名接入

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Serverless Framework 自定义域名(Custom Domains)完整指南:基于 AWS API Gateway、ACM 与 Route 53 的自动化域名接入

Serverless Framework 自定义域名(Custom Domains)完整指南:基于 AWS API Gateway、ACM 与 Route 53 的自动化域名接入

【免费下载链接】serverless⚡ Serverless Framework – Effortlessly build apps that auto-scale, incur zero costs when idle, and require minimal maintenance using AWS Lambda and other managed cloud services.项目地址: https://gitcode.com/GitHub_Trending/se/serverless

自定义域名接入是任何对外提供服务的 Serverless API 上线前必经的一步。本指南以本仓库(Serverless Framework v4 仓库)为背景,系统讲解其内置的 Custom Domains 功能:只需在serverless.ymlprovider层级声明domaindomains,框架即可自动完成 ACM SSL 证书申请与 DNS 校验、Route 53 记录写入以及 API Gateway 域名映射。读完本文,你将掌握单域名、多 Stage、多域名、REST/HTTP/WebSocket 三类 API 的域名配置方法,理解全部配置项的默认值与约束,并能处理第三方注册商域名、多区域部署等边缘场景。

功能背景:从社区插件到内置能力

文档 Custom Domains 明确指出:Serverless Framework v4 的自定义域名能力借鉴并致敬了 Amplify Education,并由 packages/serverless/lib/plugins/index.js 随内置插件一起加载。

这个内置插件暴露了两个独立命令与四个生命周期钩子(见 domains/index.js):

  • 命令:serverless create_domain(创建域名)、serverless delete_domain(删除域名);
  • 钩子:before:deploy:deploy(部署前创建/确认域名并把域名信息写入 CloudFormation 输出)、after:deploy:deploy(部署后建立 base path 映射)、after:info:info(展示域名摘要)、before:remove:remove(移除 base path 映射)。

插件是否介入由配置决定:只有当service.provider.domainservice.provider.domains出现时,钩子才会被触发(index.js)。也就是说,不配置域名时这些流程零开销。

快速开始

最简用法是在provider下声明domain

# serverless.yml service: my-service provider: name: aws runtime: nodejs20.x domain: api.example.com functions: hello: handler: src/hello.handler events: - httpApi: path: / method: get

然后执行:

serverless deploy

两点须知:

  • 若域名已在 Route 53 中注册并托管,框架会自动完成域名关联、SSL 证书申请与 DNS 配置;若域名在第三方注册商处,可继续使用,只需按下文 第三方注册商域名接入 完成少量手动 DNS 步骤。
  • 部署完成后,由于 DNS 传播与 SSL 证书激活需要时间,自定义域名通常需要 2–5 分钟才能完全生效。

从源码看,这套"自动"背后是 createDomain 的分支逻辑:先查询 API Gateway 中是否已存在该自定义域名;若不存在且未显式给出certificateArn,则先尝试在 ACM 中查找可用证书,找不到时若判定域名由 Route 53 托管,就自动请求新证书、写入 DNS 校验记录并等待校验通过,最后创建自定义域名并 UPSERT Route 53 记录。

基本用法

单一域名

字符串形式的domain是最直接的写法(内部会被转换为{ name: <domain> },见 index.js):

# serverless.yml service: my-service provider: name: aws runtime: nodejs20.x domain: api.example.com functions: hello: handler: src/hello.handler events: - httpApi: path: / method: get

多 Stage 差异化域名

利用 Serverless Framework 的params按 Stage 注入域名,避免每个环境重复维护配置:

# serverless.yml service: my-service params: dev: domain: api.example.dev prod: domain: api.example.com provider: name: aws runtime: nodejs20.x domain: ${param:domain} functions: hello: handler: src/hello.handler events: - httpApi: path: / method: get

多域名

同一服务需要暴露多个域名时使用domains数组:

# serverless.yml service: my-service provider: name: aws runtime: nodejs20.x domains: - api.example.com - api-v2.example.com functions: hello: handler: src/hello.handler events: - httpApi: path: / method: get

domaindomains甚至可以同时使用——初始化阶段插件会把二者拼接后统一处理(index.js),每个配置项都可以是字符串或对象,字符串一律视为{ name }

高级用法:对象式配置

当你需要控制 base path、API 类型或端点类型时,把domain写成对象:

service: your-service provider: name: aws runtime: nodejs20.x domain: name: api.example.com basePath: v1 apiType: http endpointType: regional functions: hello: handler: src/hello.handler events: - httpApi: path: /users method: get websocket: handler: src/websocket.handler events: - websocket: route: $connect

注意:一个服务中 HTTP、REST、WebSocket 三种 API 各只能存在一个实例,而domain只能映射到其中一个 API,所以当你混合部署多种 API 并需要分别绑定域名时,应改用domains列表(见下文"多域名配置")。

域名配置写法:字符串与对象

字符串格式(简单场景)

provider: domain: api.example.com

对象格式(进阶控制)

provider: domain: name: api.example.com # 必填:自定义域名 basePath: v1 # 可选:API 映射的 base path apiType: http # 可选:http、rest、websocket endpointType: regional # 可选:regional、edge accessMode: strict # 可选:仅 REST(单层 basePath)时生效

多域名对象写法

provider: domains: - name: api.example.com apiType: http basePath: v1 - name: api-staging.example.com apiType: http basePath: v1 - name: websocket.example.com apiType: websocket

配置项参考表

以下是domain/domains条目支持的全部配置选项:

选项类型必填说明
namestring自定义域名(如api.example.com)。兼容旧的domainName键名(domain-config.js)
basePathstringAPI 映射的 base path(如v1api)。默认值为(none)(不映射)
apiTypestringAPI 类型:httprestwebsocket,默认随服务自动检测为http(无 API 时回退 http)。每种 API 类型每服务仅一个,因此指定后会映射到对应 API
endpointTypestring端点类型:regionaledge,默认regional
certificateArnstring复用已有的 ACM 证书 ARN;不提供时尝试查找或新建证书
certificateNamestring指定名称的已有 ACM 证书;提供后不再自动新建
createRoute53Recordboolean是否自动创建 Route 53 记录,第三方注册商设false,默认true
createRoute53IPv6Recordboolean是否额外创建 IPv6(AAAA)Route 53 记录,默认true
hostedZoneIdstringRoute 53 Hosted Zone ID;不提供时自动探测
hostedZonePrivateboolean是否使用私有 Hosted Zone,默认false
route53Profilestring执行 Route 53 操作所使用的 AWS Profile
route53Regionstring执行 Route 53 操作所使用的 AWS 区域
route53Paramsobject透传给 Route 53 API 的附加参数(含路由策略等,见下文扩展说明)
splitHorizonDnsboolean为私有 Hosted Zone 启用 split-horizon DNS(同时写公共与私有 Zone)
securityPolicystring域名安全策略;经 API Gateway V2 的域名请使用TLS_1_2
accessModestringAPI Gateway 端点访问模式:basicstrict(仅限 API Gateway V1 管理的 REST 域名)
tlsTruststoreUristring双向 TLS(mTLS)信任库的 S3 URI
tlsTruststoreVersionstringTLS 信任库版本
enabledboolean/string是否启用该域名,可为布尔值或条件表达式字符串,默认true
allowPathMatchingboolean是否允许基于 path 匹配路由
preserveExternalPathMappingsboolean保留未被 Serverless Framework 托管的外部 path mappings,默认false

源码中的默认值与归一化规则

理解这些默认值与解析逻辑能帮你避开不少坑(参见 models/domain-config.js 与 globals.js):

  • apiType / endpointType 大小写与别名http/rest/websocketregional/edge均不区分大小写,内部统一转为大写(如HTTPREGIONAL);非法值会直接抛出DOMAIN_CONFIG_INVALID_*类错误。
  • basePath 归一化:空值取默认(none);自动去除首尾/pingsping是 AWS API Gateway 保留的健康检查路径,会打印警告(globals.js)。
  • securityPolicy 默认 TLS_1_2tls_1_0tls_1_2是旧式简写(等价TLS_1_0/TLS_1_2);TLS 1.3 只能通过形如SecurityPolicy_TLS13_2025_EDGE的增强策略原样透传;tls_1_3这类简写会被拒绝(单测见 domain-config.test.js)。
  • enabled / createRoute53Record / createRoute53IPv6Record / autoDomain / preserveExternalPathMappings:默认分别为true / true / true / true / falseenabled支持条件表达式字符串,可在多区域场景按需开关某个域名。
  • route53Params 内部扩展:除透传 API 参数外,插件识别routingPolicysimple/latency/weighted,默认simple)、setIdentifierweight(默认 200)、healthCheckIdlatency/weighted路由策略不允许与edge端点组合。
  • splitHorizonDns:仅在未指定hostedZoneId且非hostedZonePrivate时生效,此时 Route 53 记录会同时写入对应的公有 Zone 与私有 Zone。

完整高级配置示例

provider: name: aws runtime: nodejs20.x domain: name: api.example.com basePath: v1 apiType: rest endpointType: regional certificateArn: arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012 createRoute53Record: true createRoute53IPv6Record: true hostedZoneId: Z1PA6795UKMFR9 securityPolicy: TLS_1_2 accessMode: strict enabled: true allowPathMatching: false preserveExternalPathMappings: false route53Params: TTL: 300 Comment: 'Custom domain for API' functions: hello: handler: src/hello.handler events: - http: path: /users method: get

支持的 API 类型

HTTP API(默认)

HTTP API 是默认且推荐优先使用的 API 类型:

service: my-service provider: domain: name: api.example.com apiType: http functions: hello: handler: src/hello.handler events: - httpApi: path: / method: get

REST API

service: my-service provider: domain: name: api.example.com apiType: rest functions: hello: handler: src/hello.handler events: - http: path: / method: get

WebSocket API

service: my-service provider: domain: name: websocket.example.com apiType: websocket functions: connect: handler: src/websocket.connect events: - websocket: route: $connect disconnect: handler: src/websocket.disconnect events: - websocket: route: $disconnect default: handler: src/websocket.default events: - websocket: route: $default

apiType 的自动检测机制

大多数情况下你可以省略apiType。插件会扫描编译后的 CloudFormation 模板中的资源HttpApiApiGatewayRestApiWebsocketsApi来推断服务实际包含的 API 类型(index.js):

  • 未检测到任何 API 资源时回退到http
  • 恰好检测到一种类型则自动使用该类型;
  • 同时检测到多种类型(例如既有 HTTP API 又有 WebSocket API)而你又未显式声明apiType,框架会抛错并提示你为每个域名显式指定,避免域名"不知道该映射给谁"的歧义。

走 API Gateway V1 还是 V2?

理解了映射网关的分流规则,就能解释文末许多"注意事项"的由来。源码中的判定逻辑是(index.js):HTTP 与 WebSocket 一律走 API Gateway V2;REST 单层 basePath 走 V1,REST 多层 basePath(basePath 含/,如v1/test)也走 V2

Base Path 映射

通过basePath可以把 API 挂载到域名的特定路径前缀下:

service: my-service provider: domain: name: api.example.com basePath: v1 functions: users: handler: src/users.handler events: - httpApi: path: /users method: get

配置生效后,API 将可从https://api.example.com/v1/users访问。base path 映射的实际创建/更新发生在部署之后由 setupBasePathMappings 执行:先通过 CloudFormation 找到当前 API 的 ID,再比对已有映射(allowPathMatching: true时按 basePath 匹配,否则按 apiId 匹配),不存在则创建、存在则更新。

端点类型:Regional 与 Edge

Regional(推荐)

Regional 端点对来自同一 AWS 区域的请求做了优化:

provider: domain: name: api.example.com endpointType: regional

Edge

Edge 端点通过 CloudFront 进行全球分发:

provider: domain: name: api.example.com endpointType: edge

两点与 Edge 相关的关键限制(源码级证据):

  • 证书区域:Edge 端点的 ACM 证书必须在us-east-1创建。源码中 ACM 客户端在edge时会强制使用us-east-1(默认区域),而regional时使用当前部署区域(acm-wrapper.js);查找证书失败时的报错信息也会专门提示这一点。
  • HTTP / WebSocket 不支持 Edge:校验逻辑会直接抛出DOMAIN_VALIDATION_INCOMPATIBLE_ENDPOINT_TYPE(index.js)。原因是 AWS API Gateway 的 HTTP/WebSocket API 本身不支持 edge 分发,因此这类域名必须使用regional,或改用 REST。

多域名配置(组合不同类型 API)

复杂应用常需要让 HTTP、WebSocket、REST 各自绑定不同域名,配置如下:

service: my-service provider: name: aws runtime: nodejs20.x domains: - name: api.example.com apiType: http basePath: v1 - name: websocket.example.com apiType: websocket - name: admin.example.com apiType: rest basePath: admin functions: # HTTP API functions getUsers: handler: src/users.get events: - httpApi: path: /users method: get # WebSocket functions connect: handler: src/websocket.connect events: - websocket: route: $connect # REST API functions adminPanel: handler: src/admin.panel events: - http: path: /dashboard method: get

创建与删除时,插件会对domains内所有条目并行执行操作(Promise.all,见 index.js)。

安全策略与对账(Reconciliation)要点

配置域名安全策略时请牢记以下规则:

  • accessMode仅适用于由 API Gateway V1 管理的 REST 域名(即单层 basePath 的 REST);
  • accessModebasicstrict)要求配套使用形如SecurityPolicy_*的增强securityPolicy;校验不通过会抛出DOMAIN_VALIDATION_INCOMPATIBLE_ACCESS_MODE
  • accessMode不支持httpwebsocketAPI;
  • 走 API Gateway V2 的域名(HTTP、WebSocket,以及多层 basePath 的 REST),securityPolicy请使用TLS_1_2;显式配置其他值时会被校验逻辑拒绝(DOMAIN_VALIDATION_INCOMPATIBLE_SECURITY_POLICY);
  • 当自定义域名已存在时,框架仅对显式配置的securityPolicyaccessMode执行对账(reconciliation),未配置的字段保持不变;
  • 对 API Gateway V2 的securityPolicy对账,框架会从配置的certificateArn或已存在的域名对象中解析证书上下文后再发送更新请求。

前置条件

Route 53 Hosted Zone

域名必须在你 AWS 账户中存在对应的 Route 53 Hosted Zone。满足条件后,框架将自动完成:

  1. 为域名创建 ACM SSL 证书;
  2. 通过 DNS 记录完成证书校验;
  3. 创建所需的 Route 53 DNS 记录;
  4. 把域名映射到你的 API Gateway。

Hosted Zone 的自动探测逻辑位于 route53-wrapper.js:列出账户全部 Hosted Zone 后,用"域名后缀匹配 + 最长 Zone 名优先"的方式选择目标 Zone;指定了hostedZoneId则直接使用,指定hostedZonePrivate则只匹配对应公有/私有类型的 Zone,找不到匹配 Zone 时抛出ROUTE53_HOSTED_ZONE_NOT_FOUND

域名所有权

你必须拥有该域名,并将其 DNS 托管在 Route 53(或按下文第三方注册商流程手动托管)。

第三方注册商域名接入(手动证书与 DNS)

当域名注册在第三方注册商(而非 Route 53)时,需要手动创建并配置 SSL 证书与 DNS 记录。

第 1 步:手动申请 SSL 证书

  1. 在 AWS 控制台打开 AWS Certificate Manager(ACM);
  2. 为你的域名申请新证书;
  3. 选择 DNS 校验方式;
  4. 把校验用的 CNAME 记录添加到注册商处的 DNS 设置;
  5. 等待证书校验通过并签发;
  6. 复制证书 ARN。

第 2 步:配置 Serverless Framework

填入证书 ARN,并关闭自动 Route 53 记录创建:

service: my-service provider: name: aws runtime: nodejs20.x domain: name: api.example.com certificateArn: arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012 createRoute53Record: false apiType: http endpointType: regional functions: hello: handler: src/hello.handler events: - httpApi: path: / method: get

配置了certificateArn后,源码中的 createDomain 会跳过证书查找与自动创建;createRoute53Record: false则让 Route 53 记录写入整体跳过(route53-wrapper.js)。

第 3 步:手动创建 DNS 记录

服务部署完成后,需要在注册商处创建解析记录:

  1. 执行serverless deploy
  2. 从部署输出中记下 API Gateway 域名;
  3. 在注册商的 DNS 设置中创建:
    • 类型:CNAME(若用别名记录则 A/AAAA)
    • 名称:你的子域(如api.example.comapi
    • :API Gateway 域名(如d-1234567890.execute-api.us-east-1.amazonaws.com

提示:部署完成后可用serverless info查看域名摘要(该命令同样在after:info:info时打印每个域名的状态),或从 CloudFormation 输出的DistributionDomainName取值。源码会把DomainNameDistributionDomainNameHostedZoneId(HTTP/WebSocket 场景带Http/Websocket后缀)注入 CloudFormation Outputs,便于下游引用(index.js)。

第三方注册商配置项速查

选项必填说明
certificateArn手动创建的 ACM 证书 ARN
createRoute53Record设为false,阻止自动创建 Route 53 记录
name自定义域名
apiTypeAPI 类型(http、rest、websocket),默认 http
endpointType端点类型(regional、edge),默认 regional
basePathAPI 映射的 base path
accessModebasicstrict(仅 REST + 单层 basePath)

第三方注册商下的多域名示例

provider: name: aws runtime: nodejs20.x domains: - name: api.example.com certificateArn: arn:aws:acm:us-east-1:123456789012:certificate/api-cert-12345 createRoute53Record: false apiType: http - name: websocket.example.com certificateArn: arn:aws:acm:us-east-1:123456789012:certificate/ws-cert-12345 createRoute53Record: false apiType: websocket

SSL 证书:自动创建、复用与续期

框架对证书的处理策略可以总结为"先复用、再自动创建"(index.js 与 acm-wrapper.js):

  • 证书复用:未显式提供certificateArn时,先在 ACM 中按certificateName(若指定)或按域名精确匹配查找有效期内证书;找到即复用。Edge 端点下,会额外提示证书必须位于us-east-1
  • 自动创建:找不到有效证书且未指定certificateName时,先检查域名是否托管在 Route 53;若是,则自动申请 ACM 证书(DNS 校验方式)、把校验 CNAME 记录写入对应 Hosted Zone(TTL 300),并轮询等待证书校验通过后再创建自定义域名。域名不在 Route 53 时则无法自动创建,需要走第三方注册商流程。
  • 自动续期:证书签发后由 AWS ACM 服务本身负责自动续期,框架无需干预。
  • 双向 TLS(mTLS):如需 mTLS,配置tlsTruststoreUri(形如s3://bucket-name/key-name的 S3 URI)与可选tlsTruststoreVersion。注意 edge 端点不支持 mTLS,配置会被校验拒绝;创建前插件还会通过 S3 客户端确认信任库对象真实存在(index.js)。

部署过程与底层调用链

一次携带自定义域名配置的serverless deploy实际触发两条插件流水线:

  1. 部署前(before:deploy:deploy:并行处理每个域名——默认(autoDomain: true)先走 createDomain(必要时自动申请证书、等待校验、创建 API Gateway 自定义域名、UPSERT Route 53 A/AAAA 别名记录),随后轮询等待域名在 API Gateway 侧就绪(默认最长 120 秒、每 3 秒一次,可用autoDomainWaitFor调整),最后把域名信息写入 CloudFormation Outputs;
  2. 部署后(after:deploy:deploy:执行 base path 映射的创建/更新(setupBasePathMappings),并打印域名摘要(含各域名的分发域名与 Hosted Zone)。

与之相对,serverless remove会先移除 base path 映射,若autoDomain为真且无外部映射残留,还会顺带删除自定义域名与 Route 53 记录;preserveExternalPathMappings: true则用于保留非本服务管理的映射不误删(removeBasePathMappings)。

日常维护则可以直接使用serverless create_domain/serverless delete_domain两个独立命令做前置创建或单独清理。

限制

  • 自定义域名仅支持部署到 AWS 的服务;
  • 全自动流程要求域名由 Route 53 托管(第三方注册商需手动证书与 DNS);
  • 证书校验可能需要几分钟;
  • Edge 端点的自定义域名要求证书位于us-east-1区域;
  • 一个服务中 HTTP、REST、WebSocket 各只能有一个 API 实例,多种 API 共存时请使用domains并显式指定apiType

故障排查

证书校验失败

若证书校验失败:

  1. 确认域名存在对应的 Route 53 Hosted Zone(找不到时会报ROUTE53_HOSTED_ZONE_NOT_FOUND);
  2. dignslookup验证 DNS 传播;
  3. 检查校验记录是否被正确创建(自动流程中校验 CNAME 的 TTL 为 300 秒)。

域名部署后无法访问

若部署后域名仍不可访问:

  1. 等待 DNS 传播(最长可达 48 小时);
  2. 确认 Route 53 记录已创建(A 与 AAAA 别名记录指向 API Gateway 分发域名);
  3. 检查 API Gateway 中的域名配置与 base path 映射状态;
  4. 确认函数本身已正确部署并可经由默认 API 端点访问。

多区域部署同一域名

使用同一域名做多区域部署时:

  1. 使用 regional 端点避免冲突;
  2. 考虑每个区域使用不同子域;
  3. 用 Route 53 路由策略(如延迟latency或加权weighted,经route53Params.routingPolicy配置)做流量分发。

延伸阅读

想深入理解本文涉及的实现细节,可以在本仓库内继续阅读:

  • 功能文档:自定义域名官方指南 domains.md;
  • 插件主入口与生命周期:packages/serverless/lib/plugins/aws/domains/index.js
  • 配置模型与默认值归一化:packages/serverless/lib/plugins/aws/domains/models/domain-config.js.../models/domain-info.js
  • 常量定义(API 类型、端点类型、TLS 版本、保留 basePath 等):packages/serverless/lib/plugins/aws/domains/globals.js
  • AWS 服务封装:packages/serverless/lib/plugins/aws/domains/aws/下的acm-wrapper.jsroute53-wrapper.jsapi-gateway-v1-wrapper.jsapi-gateway-v2-wrapper.jscloud-formation-wrapper.jss3-wrapper.js
  • 单元测试(校验默认值与错误分支):packages/serverless/test/unit/lib/plugins/aws/domains/models/domain-config.test.js.../models/domain-info.test.js

【免费下载链接】serverless⚡ Serverless Framework – Effortlessly build apps that auto-scale, incur zero costs when idle, and require minimal maintenance using AWS Lambda and other managed cloud services.项目地址: https://gitcode.com/GitHub_Trending/se/serverless

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 19:57:19

多微源并联下垂控制设计要点与工程实践解析

1. 从主从到对等&#xff1a;为什么多机并联离不开下垂控制做过多微源并联项目的人都清楚&#xff0c;最头疼的问题不是单机并网&#xff0c;而是多台逆变器同时挂在一条母线上时&#xff0c;功率怎么分配、环流怎么抑制、系统怎么保持稳定。前些年主流方案是主从控制&#xff…

作者头像 李华
网站建设 2026/9/9 19:57:12

智能体循环工程:设计自主驱动的AI闭环系统

最近被问得最多的一个问题&#xff0c;已经不再是“智能体怎么搭建”&#xff0c;而是“智能体跑通了&#xff0c;但它怎么才能真正自己干活”。我理解这个变化背后的真实需求&#xff1a;一次对话、一次工具调用、一个单轮任务&#xff0c;哪怕回答再漂亮&#xff0c;只要下一…

作者头像 李华
网站建设 2026/9/9 19:56:16

Linux三剑客面试100题:grep、sed、awk高频考点深度解析

带过不少新人&#xff0c;也当过很多次面试官&#xff0c;Linux 命令行这一关几乎是必考的。而在命令行里&#xff0c;grep、sed、awk 这三兄弟又是绝对的主角。市面上讲三剑客的教程一抓一大把&#xff0c;但真正能对着面试题把原理、用法、坑点讲透的很少。这套 100 道题&…

作者头像 李华
网站建设 2026/9/9 19:53:35

双框架实践:ThinkPHP+Laravel构建口腔门诊管理系统

口腔门诊管理系统这类型的项目&#xff0c;在毕业设计和中小型外包里出现频率非常高。但大多数网上能搜到的所谓“源码”都存在一个问题&#xff1a;要么是单纯的增删改查demo&#xff0c;要么业务逻辑根本撑不起真实门诊的运转。这段时间我把一套基于ThinkPHP和Laravel双框架实…

作者头像 李华