# FYShop **Repository Path**: mythp/fyshop ## Basic Information - **Project Name**: FYShop - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-07-17 - **Last Updated**: 2026-08-03 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Klx.Shop Next ## Identity provider `src/Klx.Shop.Identity` is the self-hosted OpenIddict identity boundary. It preserves legacy numeric member IDs, uses authorization code flow with PKCE, and refuses to treat imported legacy password hashes as current credentials. Review `docs/adr/0027-self-hosted-openiddict-identity-provider.md` and `docs/deployment/identity-provider-go-live-checklist.md` before deployment. Klx.Shop Next 是现有 Klxmall 商城的独立重建版本。项目不使用 CoreShop 或其他商城项目的源代码,以现有系统的实际业务规则、合法持有的数据和验收样例作为需求来源。 项目会研究CoreShop等成熟商城公开展示的优秀产品思路和通用架构模式,再结合现有业务独立设计、编码和验证。详细边界见[ADR-0002](docs/adr/0002-benchmark-and-independent-redesign.md)。 ## 技术基线 - .NET 10 LTS - ASP.NET Core Minimal API - 模块化单体 - MySQL(第二迭代引入持久化) - Redis(第二迭代引入缓存、锁和幂等) - 后台任务(第二迭代引入) - Uni-App 与管理端通过 API 接入 ## 项目结构 ```text src/ Klx.Shop.Api HTTP 接口和宿主 Klx.Shop.Application 用例、端口和事务边界 Klx.Shop.Domain 领域模型和业务不变量 Klx.Shop.Infrastructure 数据库及外部系统适配 tools/ Klx.Shop.Migration 数据迁移工具 tests/ Klx.Shop.Domain.Specs 无外部测试包的领域规格测试 docs/ adr/ 架构决策记录 ``` 依赖方向固定为: ```text Api -> Application <- Infrastructure | v Domain ``` Domain 不依赖数据库、Web框架或第三方商城系统。 每个核心模块开始开发前必须使用[模块设计评审模板](docs/module-design-review-template.md),并遵循[可持续演进技术护栏](docs/engineering-guardrails.md)。 ## 本地验证 ```powershell dotnet build Klx.Shop.slnx dotnet run --project tests/Klx.Shop.Api.Specs dotnet run --project tests/Klx.Shop.Domain.Specs dotnet run --project tests/Klx.Shop.Checkout.Specs dotnet run --project tests/Klx.Shop.Inventory.Specs dotnet run --project tests/Klx.Shop.Legacy.Specs dotnet run --project tests/Klx.Shop.MemberAddress.Specs dotnet run --project tests/Klx.Shop.Payment.Specs dotnet run --project tests/Klx.Shop.OrderExpiration.Specs dotnet run --project tests/Klx.Shop.OrderQuery.Specs dotnet run --project tests/Klx.Shop.Outbox.Specs dotnet run --project tests/Klx.Shop.Persistence.Specs dotnet run --project tests/Klx.Shop.ShoppingCart.Specs dotnet run --project tests/Klx.Shop.MySql.Specs dotnet run --project src/Klx.Shop.Api ``` 启动 API 后可访问: - `GET /health/live` - `GET /health/ready` - `GET /api/system/info` - `GET /api/v1/catalog/products` - `GET /api/v1/catalog/products/{id}` - `GET /api/v1/catalog/categories` - `GET /api/v1/identity/member` - `GET /api/v1/identity/merchant` Health probes return a versioned JSON contract with aggregate and fixed-name check statuses, durations and the request Trace ID. Readiness returns HTTP 503 for both degraded and unhealthy results. Diagnostic descriptions, exceptions and arbitrary check data are never returned. Commerce API availability: The routes below are available in Development. Outside Development, the core commerce and database-backed finance routes are also mapped when `ConnectionStrings:ShopDatabase` is configured. Payment intents, provider refund/payout execution, payout reconciliation, every `simulated` callback and maintenance routes remain Development-only until reviewed production provider adapters are installed. See [production composition v1](docs/reviews/production-composition-v1.md). - `POST /api/v1/checkout/orders` - `POST /api/v1/checkout/batches` - `GET /api/v1/orders` - `GET /api/v1/orders/{orderId}` - `GET /api/v1/merchant/orders` - `GET /api/v1/merchant/orders/{orderId}` - `GET /api/v1/member/order-archive` - `GET /api/v1/member/order-archive/{legacyOrderId}` - `POST /api/v1/orders/{orderId}/cancel` - `POST /api/v1/orders/{orderId}/confirm-receipt` - `GET /api/v1/member/addresses` - `GET /api/v1/member/profile` - `GET /api/v1/member/finance-archive` - `GET /api/v1/merchant/finance-archive` - `GET /api/v1/merchant/settlements/summary` - `GET /api/v1/merchant/settlements/entries` - `GET /api/v1/merchant/settlements/batches` - `POST /api/v1/merchant/withdrawals` - `GET /api/v1/merchant/withdrawals` - `POST /api/v1/merchant/withdrawals/{withdrawalId}/cancel` - `GET /api/v1/admin/withdrawals` - `POST /api/v1/admin/withdrawals/{withdrawalId}/review` - `POST /api/v1/admin/withdrawals/{withdrawalId}/payout` - `POST /api/v1/admin/payout-reconciliations` - `GET /api/v1/admin/payout-reconciliations` - `GET /api/v1/admin/payout-reconciliation-cases` - `GET /api/v1/admin/payout-reconciliation-cases/metrics` - `POST /api/v1/admin/payout-reconciliation-cases/{caseId}/assign` - `POST /api/v1/admin/payout-reconciliation-cases/{caseId}/notes` - `POST /api/v1/admin/payout-reconciliation-cases/{caseId}/resolve` - `POST /api/v1/admin/payout-reconciliation-cases/{caseId}/reopen` - `GET /api/v1/admin/payout-reconciliation-cases/{caseId}/events` - `POST /api/v1/withdrawals/simulated/callbacks` - `POST /api/v1/refunds` - `GET /api/v1/refunds` - `POST /api/v1/refunds/{refundId}/cancel` - `GET /api/v1/merchant/refunds` - `POST /api/v1/merchant/refunds/{refundId}/review` - `POST /api/v1/refunds/{refundId}/returns` - `POST /api/v1/merchant/refunds/{refundId}/returns/receive` - `POST /api/v1/merchant/coupons/campaigns` - `POST /api/v1/merchant/coupons/campaigns/{campaignId}/publish` - `POST /api/v1/coupons/{campaignId}/claim` - `GET /api/v1/coupons/mine` - `GET /api/v1/merchant/products` - `POST /api/v1/merchant/products` - `PUT /api/v1/merchant/products/{productId}` - `POST /api/v1/merchant/products/{productId}/submit` - `POST /api/v1/merchant/products/{productId}/publish` - `POST /api/v1/merchant/products/{productId}/take-off-sale` - `GET /api/v1/admin/products/pending` - `POST /api/v1/admin/products/{productId}/review` - `GET /api/v1/admin/catalog/categories` - `POST /api/v1/admin/catalog/categories` - `PUT /api/v1/admin/catalog/categories/{categoryId}` - `POST /api/v1/admin/catalog/categories/{categoryId}/activate` - `POST /api/v1/admin/catalog/categories/{categoryId}/deactivate` - `GET /api/v1/admin/catalog/categories/{categoryId}/events` - `GET /api/v1/merchant/shop` - `GET /api/v1/admin/shops` - `GET /api/v1/admin/shops/{shopId}` - `POST /api/v1/admin/shops` - `PUT /api/v1/admin/shops/{shopId}` - `POST /api/v1/admin/shops/{shopId}/review` - `POST /api/v1/admin/shops/{shopId}/suspend` - `POST /api/v1/admin/shops/{shopId}/resume` - `GET /api/v1/admin/shops/{shopId}/events` - `GET /api/v1/merchant/freight-templates` - `PUT /api/v1/merchant/freight-templates/{templateId}` - `POST /api/v1/merchant/freight-templates/{templateId}/activate` - `POST /api/v1/merchant/freight-templates/{templateId}/deactivate` - `POST /api/v1/merchant/freight-templates/{templateId}/preview` - `GET /api/v1/merchant/logistics/carriers` - `PUT /api/v1/merchant/logistics/carriers/{carrierCode}` - `GET /api/v1/member/orders/{orderId}/tracking` - `GET /api/v1/merchant/orders/{orderId}/tracking` - `POST /api/v1/logistics/tracking/simulated/callbacks` (Development only) - `GET /api/v1/merchant/inventory` - `GET /api/v1/merchant/inventory/{skuId}/movements` - `POST /api/v1/merchant/inventory/{skuId}/receipts` - `POST /api/v1/merchant/inventory/{skuId}/adjustments` - `PUT /api/v1/merchant/inventory/{skuId}/safety-stock` - `GET /api/v1/member/addresses/{addressId}` - `POST /api/v1/member/addresses` - `PUT /api/v1/member/addresses/{addressId}` - `PUT /api/v1/member/addresses/{addressId}/default` - `DELETE /api/v1/member/addresses/{addressId}` - `POST /api/v1/fulfillment/orders/{orderId}/ship` - `GET /api/v1/cart` - `PUT /api/v1/cart/items/{skuId}` - `PUT /api/v1/cart/items/{skuId}/selection` - `PUT /api/v1/cart/selection` - `DELETE /api/v1/cart/items/{skuId}` - `DELETE /api/v1/cart` - `POST /api/v1/payments/intents` - `POST /api/v1/payments/simulated/callbacks` - `POST /api/v1/maintenance/order-expirations/run` - `GET /api/v1/maintenance/outbox/pending` - `POST /api/v1/maintenance/outbox/dispatch` - `POST /api/v1/maintenance/settlements/run` - `POST /api/v1/maintenance/payout-case-sla/run` Member-owned Development endpoints require the local-only header: ```http X-FYShop-Member-Id: 1001 ``` Development merchant fulfillment additionally requires: ```http X-FYShop-Shop-Id: 1001 ``` Development withdrawal approval additionally requires: ```http X-FYShop-Admin-Id: 7001 ``` Do not send `memberId` in checkout or payment JSON. The Development header is not registered outside Development. Production validates JWT access tokens and expects a positive numeric `sub` claim for members, `shop_id` for merchants, or `admin_id` for administrators. Merchant authorization also reads the authoritative shop registry on every request. The shop must be Active and its optional contract must not be expired; missing or suspended shops and lookup failures are denied. Development without MySQL keeps the local-header workflow, while Development with MySQL uses the same operational gate as production. Public MySQL catalog and checkout reads use the same shop state. Products from missing, suspended or expired shops are hidden, treated as not on sale, and revalidated under a shop-row lock before a new order is committed. Historical orders and after-sales records are not rewritten by a later suspension. Production identity settings are non-secret environment configuration: Production configuration uses explicit `__REPLACE_BEFORE_GO_LIVE__:*` placeholders and refuses to start until every loaded placeholder is replaced. The external-provider template and the release sign-off list are documented in [the go-live replacement checklist](docs/deployment/go-live-replacement-checklist.md). ```powershell $env:Authentication__Authority = "https://identity.example.com" $env:Authentication__Audience = "fyshop-api" $env:Authentication__RequireHttpsMetadata = "true" $env:Authentication__ClockSkewSeconds = "60" $env:Cors__AllowedOrigins__0 = "https://admin.example.com" $env:Cors__AllowedOrigins__1 = "https://shop.example.com" $env:AllowedHosts = "api.example.com" $env:ReverseProxy__Enabled = "true" $env:ReverseProxy__TrustedSources__0 = "10.20.0.0/24" $env:ReverseProxy__ForwardLimit = "1" $env:Hsts__MaxAgeDays = "180" $env:Hsts__IncludeSubDomains = "false" $env:Hsts__Preload = "false" ``` Missing or invalid production identity configuration prevents startup. Token signing keys are obtained through the authority metadata and must not be stored in this repository. Production CORS accepts only explicitly configured HTTPS origins. Wildcards, paths, credentials embedded in URLs, duplicate origins, and an empty origin list prevent startup. Development permits explicitly configured HTTP origins for local frontends. The policy allows API methods and request headers, exposes `Location`, `Retry-After`, and `X-Trace-Id`, and does not enable cross-origin credentials. Production must explicitly choose direct TLS (`ReverseProxy__Enabled=false`) or a trusted reverse proxy. When reverse proxy support is enabled, every accepted proxy must be listed as an exact IP or CIDR under `TrustedSources`; forwarded headers from all other sources are ignored. The application requires symmetric `X-Forwarded-For`, `X-Forwarded-Proto`, and `X-Forwarded-Host` chains, limits their processed depth, and validates forwarded hosts against exact `AllowedHosts` values. HSTS defaults to 180 days. Preload requires subdomain coverage and at least a one-year max age, and should be enabled only after the entire domain has passed deployment review. Background jobs are disabled by default. They can be enabled independently: ```powershell $env:BackgroundJobs__OrderExpiration__Enabled = "true" $env:BackgroundJobs__OrderExpiration__IntervalSeconds = "30" $env:BackgroundJobs__OrderExpiration__BatchSize = "100" $env:BackgroundJobs__OutboxDispatch__Enabled = "true" $env:BackgroundJobs__OutboxDispatch__IntervalSeconds = "5" ``` Production order expiration also requires `ConnectionStrings__ShopDatabase` and `Commerce__DefaultInventoryLocationId`. Production Outbox dispatch refuses to start until a real integration-event publisher is registered; the Development logging publisher is never used as a production fallback. Authenticated commerce endpoints use identity-partitioned sliding-window rate limits. Defaults are 120 member requests, 10 checkout requests, and 60 merchant requests per 60 seconds. Configure them with `RateLimiting__Member__*`, `RateLimiting__Checkout__*`, and `RateLimiting__Merchant__*`; invalid values prevent startup. Rejected requests return HTTP 429 in the standard API envelope and include `Retry-After` when the limiter provides it. Every API request uses W3C trace identifiers. Responses expose the identifier as `X-Trace-Id`, and API envelopes use the same value. Safe structured request logs contain only method, route template, status, duration, exception type and trace ID; they never include raw paths, query strings, headers, bodies or exception messages. The `Klx.Shop.Api` meter publishes request count, duration and safely handled unhandled-exception count. Unexpected exceptions return a standard `system.unexpected_error` envelope without leaking internal details. The same meter also publishes `klx.shop.commerce.operation.count` for stable checkout, payment, refund, return and inventory mutation operations. Its only dimensions are a predefined operation name and `succeeded`, `rejected`, `failed`, or `other`. Outbox processing publishes delivered, retry and dead-letter message counts plus dispatch-cycle failures. No metric contains an order, member, shop, SKU, provider reference, amount or exception message. This in-process baseline does not install a telemetry exporter. Production OTLP export, dashboards, alerts and retention remain explicit go-live work under `ExternalIntegrations:Observability`. ## Migration analysis ```powershell dotnet run --project tools/Klx.Shop.Migration -- ` catalog-dump --file ../artEditor/web/msc_shop.sql --limit 500 dotnet run --project tools/Klx.Shop.Migration -- ` shop-dump ` --file ../artEditor/web/msc_shop.sql ` --output artifacts/migration/shop-report.json dotnet run --project tools/Klx.Shop.Migration -- ` shop-import-dump ` --file ../artEditor/web/msc_shop.sql ` --apply false # Commit only after reviewing shop-report.json and the dry-run result. dotnet run --project tools/Klx.Shop.Migration -- ` shop-import-dump ` --file ../artEditor/web/msc_shop.sql ` --apply true dotnet run --project tools/Klx.Shop.Migration -- ` catalog-import-dump ` --file ../artEditor/web/msc_shop.sql ` --limit 500 ` --apply false # Commit only after the dry-run succeeds. dotnet run --project tools/Klx.Shop.Migration -- ` catalog-import-dump ` --file ../artEditor/web/msc_shop.sql ` --limit 500 ` --apply true dotnet run --project tools/Klx.Shop.Migration -- ` freight-dump ` --file ../artEditor/web/msc_shop.sql ` --regions ../src/Klxmall.Web/Scripts/region.json ` --historical-regions ../src/Klxmall.Web/Scripts/regionbak.json dotnet run --project tools/Klx.Shop.Migration -- ` freight-import-dump ` --file ../artEditor/web/msc_shop.sql ` --regions ../src/Klxmall.Web/Scripts/region.json ` --historical-regions ../src/Klxmall.Web/Scripts/regionbak.json ` --apply false # Commit only after the dry-run succeeds. dotnet run --project tools/Klx.Shop.Migration -- ` freight-import-dump ` --file ../artEditor/web/msc_shop.sql ` --regions ../src/Klxmall.Web/Scripts/region.json ` --historical-regions ../src/Klxmall.Web/Scripts/regionbak.json ` --apply true $env:KLX_LEGACY_MYSQL = "Server=...;Database=...;User ID=readonly;Password=..." dotnet run --project tools/Klx.Shop.Migration -- catalog-mysql ``` Freight analysis intentionally combines the current region hierarchy with the historical region snapshot. The legacy database contains freight templates created against both identifier generations; historical IDs are translated to the current hierarchy by their validated parent/name path. Reports retain every legacy template, rule, area-detail, and product identifier. The live database command rejects accounts that are not strictly read-only. Connection strings are never accepted as command-line arguments or written to reports. Freight import is restricted to local `fyshop_dev`, obtains a database import lock, rejects reports containing errors, and defaults to a rolled-back dry-run. It does not require temporary-table or schema-change privileges. Catalog import has the same local-only, locked, dry-run-first behavior. It imports the legacy category hierarchy, products, SKUs, flexible options, cost traceability, freight bindings, and opening stock. Re-running it updates descriptive Catalog data and safety stock but never resets live on-hand or reserved inventory. Shop import maps the authoritative `mall_shop` operational profile into `merchant_shop`. Only legacy status `Open (7)` becomes Active; every other state remains non-operational. The import replaces migration-0046 compatibility shells, is dry-run-first and idempotent, and records source/target hashes so a rerun refuses to overwrite a profile edited by an administrator. Reports omit contact phone values. Missing legacy names and contacts remain visible as go-live placeholders and are counted in the report. ## New schema migration Schema application is restricted to local `fyshop_dev` and reads credentials only from an environment variable: ```powershell $env:KLX_SHOP_MYSQL = "Server=127.0.0.1;Database=fyshop_dev;User ID=...;Password=..." dotnet run --project tools/Klx.Shop.Migration -- schema-apply dotnet run --project tools/Klx.Shop.Migration -- schema-seed-development ``` Applied migration filenames and SHA-256 checksums are recorded. Never edit an applied migration; add a new numbered script instead. With `KLX_SHOP_MYSQL` configured, Development automatically uses MySQL for shopping carts, checkout request fingerprints, orders, inventory reservations, payments, callback Inbox, and Outbox. The optional `Klx.Shop.MySql.Specs` suite validates the complete workflow inside a transaction that is rolled back after its assertions. It also runs committed, isolated concurrent reservations against a dedicated stock row and deletes every fixture row after verification. Every `inventory_stock_item.location_id` now references an `inventory_location` master row. The readiness endpoint rejects a missing or inactive or merchant-owned `Commerce:DefaultInventoryLocationId`; new reservations, merchant inventory writes, and return receipts repeat the location check inside their database transaction. Migration `0048` safely materializes existing opaque IDs as shared compatibility warehouses, which must be reviewed before production. Merchants manage owned warehouse/store identities through `/api/v1/merchant/inventory-locations`. Inventory list, movement, receipt, adjustment, and safety-stock calls require an explicit location ID. Migration `0049` includes that ID in operation idempotency, so a request cannot be replayed against another warehouse. Active reservations block location deactivation. Migration `0050` makes checkout location-aware without overloading the legacy numeric `ShopBranchId`: a request can select an `InventoryLocationId`, an old branch can resolve through `legacy_branch_id`, and otherwise checkout falls back to the reviewed shared default. Batch checkout splits different locations into separate orders, and every new order snapshots the selected location before its reservation is created. Accepted customer returns use that immutable order snapshot and therefore enter the original warehouse/store. Orders migrated without an unambiguous location retain the reviewed shared default as a compatibility fallback. Cart items persist the optional selection through migration `0051` and expose `PUT /api/v1/cart/items/{skuId}/inventory-location`; a JSON `null` restores automatic allocation without changing quantity or checked state. Automatic checkout allocation evaluates the complete normalized order against one location. It prefers an Active merchant-owned location with sufficient available stock, then the reviewed shared default, with stable code/ID ordering between equivalent owned locations. Reservation still locks and revalidates stock, so allocation never weakens oversell protection. Outbox dispatch defaults are non-secret settings under the `Outbox` section in `appsettings.json`. The Development dispatch endpoint uses a logging publisher; the hosted production dispatcher refuses to start until a real broker publisher is registered, without changing the transactional Outbox protocol. ## 当前迭代 当前已经完成 Catalog、会员、店铺、多仓库存、购物车、结账、订单、 履约、退款退货、优惠券、结算和提现等核心后端切片及 MySQL 持久化。 最新切片实现了购物车选仓、整单自动选仓、跨仓拆单以及退货回原仓。 下一阶段重点是生产支付/退款/打款/物流/消息适配器、前端交付、生产 数据迁移对账,以及压测、安全和灰度回滚演练。