首页 文章 精选 留言 我的

精选列表

搜索[批量导入手机通讯录],共10000篇文章
优秀的个人博客,低调大师

批量开票的事件驱动接入设计

摘要:支付即开票并不是在支付回调里直接调用开票接口。业务系统还要处理重复支付通知、订单取消、部分退款、购买方信息补充、异步开票结果和发票文件回写。本文以事件驱动方式拆解这条链路,并提供 Java 示例说明如何可靠地创建开票任务。 > 示例用于说明工程设计,接口地址、状态值和数据字段均为示意,不代表慧穗正式 API 文档。 ## 一、不要把支付回调和开票请求绑成一个同步事务 支付平台的回调需要尽快确认接收。若在回调线程中同步等待税务侧开票结果,开票延迟或网络波动会拖慢支付链路,还可能造成支付平台重复通知。 更稳妥的做法是:支付回调只确认支付事实并持久化业务事件,开票任务由后台消费者处理。系统通过业务状态机决定订单是否已达到开票条件,再向发票接口提交任务。 ```javascript ```mermaid sequenceDiagram participant Pay as 支付平台 participant Order as 订单系统 participant Queue as 事件队列 participant Invoice as 发票接口 Pay->>Order: 支付结果通知 Order->>Order: 校验并记录支付状态 Order->>Queue: 发布待开票事件 Queue->>Invoice: 提交开票任务 Invoice-->>Order: 回调开票结果 Order->>Order: 更新订单与发票状态 ``` ``` ## 二、建立订单与开票任务之间的状态关系 订单状态和发票任务状态不要混成一个字段。订单可能已经支付,但购买方信息还不完整;也可能发票开具成功后发生退款,需要按业务规则进入红字处理。 建议至少分别记录: - 业务单据状态:待支付、已支付、已取消、部分退款、已退款; - 开票任务状态:待提交、已受理、处理中、成功、失败; - 发票记录状态:蓝字票信息、红字关联、文件获取状态。 具体业务何时允许开票,应由企业的结算、退款和开票规则确定。 ## 三、用事务消息避免“订单已支付,开票事件丢失” 如果订单状态写入数据库成功,但消息发布失败,开票任务就会丢失。可以使用事务 Outbox:在同一数据库事务内更新支付状态并写入待发送事件,后台任务再把事件投递到队列。 ```javascript @Transactional public void recordPayment(PaymentNotice notice) { orderRepository.markPaid(notice.orderId(), notice.paymentId()); // 与订单状态在同一事务内落库 outboxRepository.save(new OutboxEvent( "ORDER_PAID", notice.orderId(), notice.paymentId() )); } ``` Outbox 发布器可以定时扫描未发送事件,投递成功后标记完成。若消息系统重复投递,消费者还要通过业务唯一键实现幂等。 ## 四、消费者提交开票任务:用业务键控制重复 一个订单可能收到多次支付通知,也可能因消息重试被重复消费。提交开票任务前,应通过数据库唯一约束保证同一“租户+业务单据+开票操作版本”只创建一条有效请求。 ```javascript CREATE UNIQUE INDEX uk_invoice_business_operation ON invoice_task (tenant_id, business_id, operation_version); ``` Java 侧示例: ```javascript public InvoiceTask submitIfAbsent( String tenantId, String businessId, String operationVersion, InvoiceRequest request) { return taskRepository.findByBusinessKey( tenantId, businessId, operationVersion) .orElseGet(() -> { InvoiceTask task = InvoiceTask.pending( tenantId, businessId, operationVersion); taskRepository.save(task); // 接口返回受理任务号;最终结果异步回传 ProviderTask accepted = invoiceClient.submit(request); task.markAccepted(accepted.taskId()); return taskRepository.save(task); }); } ``` 实际并发场景下,仅“先查再插入”无法防止竞争条件,数据库唯一索引才是最终约束。捕获唯一键冲突后,重新读取已有任务并返回其当前状态。 ## 五、回调处理采用“验签、落库、快速确认” 发票结果通过回调返回时,业务系统应验证签名和任务归属,再幂等写入发票结果。回调中不要执行耗时的文件归档、通知或报表处理;将后续动作写入 Outbox 或队列,由后台异步完成。 ```javascript @PostMapping("/callbacks/invoice-result") public ResponseEntity receiveResult( @RequestHeader("X-Signature") String signature, @RequestBody InvoiceResult result) { if (!callbackVerifier.verify(signature, result)) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } resultService.recordOnce( result.taskId(), result.itemId(), result.tenantId(), result.businessId(), result.status(), result.invoiceNumber() ); return ResponseEntity.ok().build(); } ``` recordOnce 应校验任务编号、租户、业务单号是否一致,并为“任务编号+明细编号”设置唯一约束。重复回调只返回成功,不重复触发订单更新和财务动作。 ## 六、退款、红字和文件获取是开票链路的一部分 自动开票上线前,至少要明确这些边界: - **订单取消或支付关闭:** 尚未提交的开票任务应取消; - **开票处理中发生退款:** 按当前任务状态决定暂停、查询或等待结果,不能简单重复提交; - **蓝字票已开后发生退款:** 进入关联原蓝字发票的红字处理流程; - **开票成功但文件未取回:** 订单发票状态保持成功,文件进入待获取队列; - **部分退款或拆单退款:** 按业务单据和原发票之间的映射规则处理。 结构化发票数据与 PDF、OFD、XML 等文件应分别保存状态,再通过发票唯一标识、业务单号和租户关联。这样文件下载故障不会覆盖开票结果。 ## 七、多租户系统如何防止跨主体串单 代账平台、集团软件和多品牌 SaaS 可能同时服务多个纳税主体。租户上下文应由认证信息和授权关系确定,并贯穿事件、任务、回调和文件访问: - 事件中保存租户与业务单号; - 消费者按授权关系加载纳税主体; - 任务查询校验租户归属; - 回调结果必须与原任务主体一致; - 文件下载只允许访问当前租户可见的记录。 不要依赖请求参数中的 taxpayerId 单独决定访问权限。 ## 八、登录组件与业务入口的衔接 有些开票流程涉及用户登录、授权或选择纳税主体。软件厂商可以把登录组件接入自己的页面或调用链路,让授权状态进入现有业务流程,避免重复开发登录页面及相关交互。具体交互由产品决定,接口侧负责按约定传递授权状态和任务上下文。 ## 九、上线验收:按业务事件回放 测试环境应覆盖支付通知重复、事件队列重放、开票接口超时、回调重复、订单取消、部分退款、红字关联及文件补取。每个测试用例都应核对订单状态、任务状态、发票记录和文件记录,确保链路可以重放和追溯。

优秀的个人博客,低调大师

2026 Python采集实战:BeautifulSoup批量抓取多页电商商品数据

单页商品抓取不难,真正容易出问题的是从第1页跑到第50页。 我做采集这些年遇到过不少类似情况:前几页运行正常,到了中间突然超时;某一页结构变化,整个程序直接退出;代理已经失效,脚本却还在对同一个地址反复重试。 所以,多页采集不能只在单页代码外面套一个`for`循环。至少要补上会话复用、有限重试、随机间隔、异常记录和CSV存储。 本文仍使用公开爬虫练习网站演示。实际采集电商平台前,应确认访问授权、服务条款和数据使用边界。 ## **先建立可复用的请求会话** 如果每一页都重新创建连接,会增加额外开销。`requests.Session()`可以复用部分连接及公共请求头: ``` import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry session = requests.Session() session.headers.update({ "User-Agent": ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/126.0.0.0 Safari/537.36" ), "Accept-Language": "zh-CN,zh;q=0.9" }) retry = Retry( total=2, connect=2, read=2, status=2, backoff_factor=0.8, status_forcelist=[429, 500, 502, 503, 504], allowed_methods=["GET"] ) adapter = HTTPAdapter(max_retries=retry) session.mount("http://", adapter) session.mount("https://", adapter) ``` 重试次数不是越多越好。我一般先设为2次,用于处理短暂网络波动。遇到403、登录提示或验证码时,应停止并检查权限与访问策略,而不是通过无限重试继续施压。 ## **编写多页商品解析逻辑** 完整示例代码如下: ``` import csv import random import time from urllib.parse import urljoin import requests from bs4 import BeautifulSoup from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry BASE_URL = "https://books.toscrape.com/catalogue/page-{}.html" OUTPUT_FILE = "ecommerce_products.csv" session = requests.Session() session.headers.update({ "User-Agent": ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/126.0.0.0 Safari/537.36" ) }) retry = Retry( total=2, backoff_factor=0.8, status_forcelist=[429, 500, 502, 503, 504], allowed_methods=["GET"] ) adapter = HTTPAdapter(max_retries=retry) session.mount("http://", adapter) session.mount("https://", adapter) def fetch_page(page_number): page_url = BASE_URL.format(page_number) response = session.get( page_url, timeout=(5, 15) ) response.raise_for_status() soup = BeautifulSoup(response.text, "html.parser") cards = soup.select("article.product_pod") products = [] for card in cards: title_tag = card.select_one("h3 a") price_tag = card.select_one(".price_color") stock_tag = card.select_one(".instock.availability") if not title_tag or not price_tag: continue products.append({ "页码": page_number, "商品名称": title_tag.get("title", "").strip(), "价格": price_tag.get_text(strip=True), "库存状态": ( stock_tag.get_text(" ", strip=True) if stock_tag else "" ), "详情链接": urljoin( page_url, title_tag.get("href", "") ) }) return products def save_products(products): fieldnames = [ "页码", "商品名称", "价格", "库存状态", "详情链接" ] with open( OUTPUT_FILE, "w", newline="", encoding="utf-8-sig" ) as file: writer = csv.DictWriter( file, fieldnames=fieldnames ) writer.writeheader() writer.writerows(products) def main(): all_products = [] failed_pages = [] for page_number in range(1, 11): try: page_products = fetch_page(page_number) all_products.extend(page_products) print( f"第{page_number}页完成," f"获取{len(page_products)}条" ) except requests.RequestException as error: failed_pages.append(page_number) print(f"第{page_number}页失败:{error}") time.sleep(random.uniform(1.5, 3.0)) save_products(all_products) print(f"共保存{len(all_products)}条商品数据") print(f"失败页码:{failed_pages}") if __name__ == "__main__": main() ``` 这段代码有一个比较实用的设计:某一页失败后,只记录失败页码,不会让整个任务立即退出。后续可以单独补采失败页面,而不必把成功页面全部重跑一遍。 ## **为什么要保留页码字段** 保存结果时,我特意增加了“页码”字段。 商品名称、价格和链接是业务数据,页码则是采集链路数据。出现数量异常时,可以直接查看是哪一页缺失。如果不保存页码,最后只知道总量少了,却很难快速定位断点。 正式任务中还可以继续增加: - 采集时间; - HTTP状态码; - 请求耗时; - 出口标识; - 重试次数。 这些字段未必都要交付给业务人员,但对排查稳定性很有帮助。 ## **代理IP应该在哪一步接入** 如果只是抓取练习网站的10个页面,没有必要为了使用代理而使用代理。 当任务扩大到大量授权页面、不同地区商品展示验证或持续价格监测时,才需要把请求出口纳入设计。我一般会把代理认证地址写入环境变量: ``` import os proxy_url = os.getenv("PROXY_URL") if proxy_url: session.proxies.update({ "http": proxy_url, "https": proxy_url }) ``` 接入极安代理时,也可以沿用这种方式。代理地址变化后只调整运行环境,不改采集逻辑,更适合长期维护。 不过,代理不能替代访问授权,也不能解决所有失败。403可能是权限或页面策略问题,429通常与请求频率有关,连接超时才更需要检查代理存活状态、认证配置和链路响应。 ## **别一开始就把并发调得很高** 不少人为了“提速”,单线程代码刚跑通就直接开20个线程。结果目标站响应变慢、429增加、代理连接数也被打满,最终实际完成速度反而下降。 我的习惯是先用低并发跑一小批页面,记录成功率和平均响应时间,再逐步增加任务量。每次只改一个参数,才能判断性能变化来自哪里。 对于BeautifulSoup多页采集来说,能稳定完成、失败可定位、结果可补采,比短时间内冲出一个很高的请求数更重要。先把单页解析做准,再把多页链路做稳,最后才轮到并发和代理优化。

资源下载

更多资源
Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册