diff --git a/docs/api-phase1.md b/docs/api-phase1.md
index a2af271..8b051b0 100644
--- a/docs/api-phase1.md
+++ b/docs/api-phase1.md
@@ -190,11 +190,22 @@ POST /app/auth/register
**可见性**:匿名与其他人只看得到 `published`;自己待审的评论也会返回,便于展示"审核中"。
评论列表禁止重复提交(同一剧集每分钟 5 条,可配)。
-### 1.8 交易、创作者(下一批)
+### 1.8 金币与权益(已实现)
+
+| 方法 | 路径 | 说明 |
+| --- | --- | --- |
+| GET | `/app/wallet` | 金币余额 + 最近流水 |
+| POST | `/app/episodes/{id}/unlock` | 金币解锁分集;幂等键 `unlock:{userId}:{episodeId}`,重复调用不重复扣币 |
+| GET | `/app/orders` | 我的订单 |
+| POST | `/app/orders/verify` | 提交 IAP / Google Play 票据。**本轮只留端口不接商店**,会返回 `200003 尚未配置` |
+
+解锁后播放入口与下载入口会同时放行 —— 权益判定由交易模块的 `EntitlementPort` 实现提供,
+播放域与下载域都不需要改代码。金币变动一律带幂等键并写 `coin_ledger`,钱包余额与流水同事务。
+
+### 1.9 创作者(下一批)
| 方法 | 路径 | 说明 |
| --- | --- | --- |
-| POST | `/app/orders/verify` | 提交 IAP / Google Play 票据,服务端二次校验后发权益(本轮只留接口,不接真实商店) |
| GET | `/app/orders` | 我的订单 |
| GET | `/app/wallet` | 金币余额与流水 |
| POST | `/app/episodes/{id}/unlock` | 金币解锁分集 |
@@ -251,7 +262,21 @@ POST /app/auth/register
| GET | `/admin/sensitive-words/page` | `sensitive-word:list` | 敏感词检索 |
| POST/PUT/DELETE | `/admin/sensitive-words[/{id}]` | `sensitive-word:update` | 维护词库,改动即时生效(服务端缓存失效重建) |
-### 2.6 订单、创作者审核、数据看板(下一批)
+### 2.6 订单与权益(已实现)
+
+| 方法 | 路径 | 权限点 | 说明 |
+| --- | --- | --- | --- |
+| GET | `/admin/orders/page` `/admin/orders/{orderNo}` `/{orderNo}/events` | `order:list` | 订单检索、详情、状态流转流水 |
+| POST | `/admin/orders/{orderNo}/compensate` | `order:update` | 补发:已支付未交付的订单重新交付,已交付幂等返回 |
+| POST | `/admin/orders/{orderNo}/refund` | `order:update` | 退款/撤销:回收权益 + 扣回金币(余额不足时留人工跟进备注) |
+| GET | `/admin/entitlements/page` | `entitlement:list` | 权益检索 |
+| POST | `/admin/entitlements` | `entitlement:update` | 人工补发权益(客服补偿) |
+| PUT | `/admin/entitlements/{id}/revoke` | `entitlement:update` | 回收权益,播放判定实时生效 |
+| GET | `/admin/wallets/{userId}/ledger` | `order:list` | 金币流水 |
+| POST | `/admin/wallets/coins` | `wallet:update` | 人工补币/扣回,必须带幂等键,先校验用户存在 |
+| POST | `/admin/reconcile/run` · GET `/admin/reconcile/page` | `reconcile:run` / `order:list` | 手动对账与历史(默认每天 04:20 自动跑前一天) |
+
+### 2.7 创作者审核、数据看板(下一批)
| 方法 | 路径 | 说明 |
| --- | --- | --- |
@@ -284,6 +309,6 @@ POST /app/auth/register
| 05 记录、收藏与下载 | 已建 | 已冻结 | **已实现**:进度、观看流水、收藏、离线下载授权与任务 |
| 06 评论、点赞与分享 | 已建 | 已冻结 | **已实现**:评论(游标翻页/审核/删除)、点赞、举报、敏感词;分享埋点在下一批 |
| 07 创作者中心 | 已建 | 已冻结 | 下一批(转码任务与产物记账已实现) |
-| 08 平台运营后台 | 已建 | 用户管理已实现 | 治理/订单/看板 下一批 |
+| 08 平台运营后台 | 已建 | 已冻结 | 用户管理、内容治理、**订单与权益已实现**;创作者审核与数据看板下一批 |
| 09 分集收费与支付 | 已建 | 已冻结 | 下一批(权益判定端口已预留,接上即生效) |
| 10 质量、数据与上线 | 已建 | 已冻结 | 压测与埋点 下一批 |
diff --git a/fruitshorts-admin-api/pom.xml b/fruitshorts-admin-api/pom.xml
index 2ba4278..e09c587 100644
--- a/fruitshorts-admin-api/pom.xml
+++ b/fruitshorts-admin-api/pom.xml
@@ -39,6 +39,10 @@
com.fruitshorts
fruitshorts-module-interaction
+
+ com.fruitshorts
+ fruitshorts-module-commerce
+
org.springframework.boot
spring-boot-starter-test
diff --git a/fruitshorts-admin-api/src/main/java/com/fruitshorts/admin/controller/EntitlementAdminController.java b/fruitshorts-admin-api/src/main/java/com/fruitshorts/admin/controller/EntitlementAdminController.java
new file mode 100644
index 0000000..5130079
--- /dev/null
+++ b/fruitshorts-admin-api/src/main/java/com/fruitshorts/admin/controller/EntitlementAdminController.java
@@ -0,0 +1,59 @@
+package com.fruitshorts.admin.controller;
+
+import com.fruitshorts.commerce.dal.entity.Entitlement;
+import com.fruitshorts.commerce.service.EntitlementService;
+import com.fruitshorts.commerce.vo.EntitlementGrantReqVO;
+import com.fruitshorts.common.pojo.PageResult;
+import com.fruitshorts.common.pojo.Result;
+import com.fruitshorts.framework.log.OperationLog;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+/** 后台权益:检索、人工补发、回收。 */
+@RestController
+@RequestMapping("/admin/entitlements")
+@RequiredArgsConstructor
+public class EntitlementAdminController {
+
+ private final EntitlementService entitlementService;
+
+ @GetMapping("/page")
+ @PreAuthorize("@ss.hasPermission('entitlement:list')")
+ public Result> page(@RequestParam(required = false) Long userId,
+ @RequestParam(required = false) String status,
+ @RequestParam(required = false) String type,
+ @RequestParam(defaultValue = "1") Integer pageIndex,
+ @RequestParam(defaultValue = "10") Integer pageSize) {
+ return Result.ok(entitlementService.page(userId, status, type, pageIndex, pageSize));
+ }
+
+ /** 客服补偿:直接给用户某集/整剧的播放权益。 */
+ @PostMapping
+ @PreAuthorize("@ss.hasPermission('entitlement:update')")
+ @OperationLog(module = "订单管理", action = "人工补发权益")
+ public Result grant(@Valid @RequestBody EntitlementGrantReqVO req) {
+ return Result.ok(entitlementService.grant(req.getUserId(), req.getDramaId(),
+ req.getEpisodeId() == null ? 0L : req.getEpisodeId(),
+ req.getType() == null ? EntitlementService.TYPE_EPISODE : req.getType(),
+ "ADMIN", ""));
+ }
+
+ /** 回收权益:退款、违规处置后用。 */
+ @PutMapping("/{id}/revoke")
+ @PreAuthorize("@ss.hasPermission('entitlement:update')")
+ @OperationLog(module = "订单管理", action = "回收权益")
+ public Result revoke(@PathVariable Long id,
+ @RequestParam(required = false) String reason) {
+ entitlementService.revoke(id, reason);
+ return Result.ok();
+ }
+}
diff --git a/fruitshorts-admin-api/src/main/java/com/fruitshorts/admin/controller/OrderAdminController.java b/fruitshorts-admin-api/src/main/java/com/fruitshorts/admin/controller/OrderAdminController.java
new file mode 100644
index 0000000..703a698
--- /dev/null
+++ b/fruitshorts-admin-api/src/main/java/com/fruitshorts/admin/controller/OrderAdminController.java
@@ -0,0 +1,69 @@
+package com.fruitshorts.admin.controller;
+
+import com.fruitshorts.commerce.dal.entity.OrderEvent;
+import com.fruitshorts.commerce.dal.entity.Orders;
+import com.fruitshorts.commerce.service.OrderService;
+import com.fruitshorts.commerce.vo.OrderQueryReqVO;
+import com.fruitshorts.common.pojo.PageResult;
+import com.fruitshorts.common.pojo.Result;
+import com.fruitshorts.framework.log.OperationLog;
+import com.fruitshorts.framework.security.SecurityUtils;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+import java.util.Map;
+
+/** 后台订单:检索、补发、退款回收。 */
+@RestController
+@RequestMapping("/admin/orders")
+@RequiredArgsConstructor
+public class OrderAdminController {
+
+ private final OrderService orderService;
+
+ @GetMapping("/page")
+ @PreAuthorize("@ss.hasPermission('order:list')")
+ public Result> page(OrderQueryReqVO req) {
+ return Result.ok(orderService.page(req));
+ }
+
+ @GetMapping("/{orderNo}")
+ @PreAuthorize("@ss.hasPermission('order:list')")
+ public Result detail(@PathVariable String orderNo) {
+ return Result.ok(orderService.getExisting(orderNo));
+ }
+
+ /** 订单状态流转流水:支付争议时的证据链。 */
+ @GetMapping("/{orderNo}/events")
+ @PreAuthorize("@ss.hasPermission('order:list')")
+ public Result> events(@PathVariable String orderNo) {
+ return Result.ok(orderService.events(orderNo));
+ }
+
+ /** 补发:已支付未交付的订单重新交付;已交付时幂等返回。 */
+ @PostMapping("/{orderNo}/compensate")
+ @PreAuthorize("@ss.hasPermission('order:update')")
+ @OperationLog(module = "订单管理", action = "补发订单")
+ public Result compensate(@PathVariable String orderNo) {
+ return Result.ok(orderService.compensate(orderNo, SecurityUtils.getUserId()));
+ }
+
+ /** 退款/撤销:回收权益并扣回金币。 */
+ @PostMapping("/{orderNo}/refund")
+ @PreAuthorize("@ss.hasPermission('order:update')")
+ @OperationLog(module = "订单管理", action = "订单退款")
+ public Result refund(@PathVariable String orderNo,
+ @RequestBody(required = false) Map body) {
+ String reason = body == null ? "" : body.getOrDefault("reason", "");
+ return Result.ok(orderService.refund(orderNo, reason, SecurityUtils.getUserId()));
+ }
+
+}
diff --git a/fruitshorts-admin-api/src/main/java/com/fruitshorts/admin/controller/ReconcileAdminController.java b/fruitshorts-admin-api/src/main/java/com/fruitshorts/admin/controller/ReconcileAdminController.java
new file mode 100644
index 0000000..19b0404
--- /dev/null
+++ b/fruitshorts-admin-api/src/main/java/com/fruitshorts/admin/controller/ReconcileAdminController.java
@@ -0,0 +1,42 @@
+package com.fruitshorts.admin.controller;
+
+import com.fruitshorts.commerce.dal.entity.ReconcileTask;
+import com.fruitshorts.commerce.service.ReconcileService;
+import com.fruitshorts.common.pojo.PageResult;
+import com.fruitshorts.common.pojo.Result;
+import com.fruitshorts.framework.log.OperationLog;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.time.LocalDate;
+
+/** 后台对账:手动触发与历史查看。 */
+@RestController
+@RequestMapping("/admin/reconcile")
+@RequiredArgsConstructor
+public class ReconcileAdminController {
+
+ private final ReconcileService reconcileService;
+
+ @GetMapping("/page")
+ @PreAuthorize("@ss.hasPermission('order:list')")
+ public Result> page(@RequestParam(defaultValue = "1") Integer pageIndex,
+ @RequestParam(defaultValue = "10") Integer pageSize) {
+ return Result.ok(reconcileService.page(pageIndex, pageSize));
+ }
+
+ /** 手动对账:默认前一天,可指定日期与渠道。 */
+ @PostMapping("/run")
+ @PreAuthorize("@ss.hasPermission('reconcile:run')")
+ @OperationLog(module = "订单管理", action = "手动对账")
+ public Result run(@RequestParam(required = false) String bizDate,
+ @RequestParam(required = false) String platform) {
+ LocalDate date = bizDate == null || bizDate.isBlank() ? null : LocalDate.parse(bizDate);
+ return Result.ok(reconcileService.run(date, platform));
+ }
+}
diff --git a/fruitshorts-admin-api/src/main/java/com/fruitshorts/admin/controller/WalletAdminController.java b/fruitshorts-admin-api/src/main/java/com/fruitshorts/admin/controller/WalletAdminController.java
new file mode 100644
index 0000000..0e3d6ce
--- /dev/null
+++ b/fruitshorts-admin-api/src/main/java/com/fruitshorts/admin/controller/WalletAdminController.java
@@ -0,0 +1,58 @@
+package com.fruitshorts.admin.controller;
+
+import com.fruitshorts.commerce.dal.entity.CoinLedger;
+import com.fruitshorts.commerce.service.WalletService;
+import com.fruitshorts.commerce.vo.CoinAdjustReqVO;
+import com.fruitshorts.common.exception.ErrorCode;
+import com.fruitshorts.common.exception.ServiceException;
+import com.fruitshorts.common.pojo.Result;
+import com.fruitshorts.framework.log.OperationLog;
+import com.fruitshorts.framework.security.SecurityUtils;
+import com.fruitshorts.user.dal.mapper.AppUserMapper;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/** 后台钱包:人工补币/扣回,全部进流水且必须带幂等键。 */
+@RestController
+@RequestMapping("/admin/wallets")
+@RequiredArgsConstructor
+public class WalletAdminController {
+
+ private final WalletService walletService;
+ private final AppUserMapper appUserMapper;
+
+ @GetMapping("/{userId}/ledger")
+ @PreAuthorize("@ss.hasPermission('order:list')")
+ public Result> ledger(@PathVariable Long userId,
+ @RequestParam(defaultValue = "20") int limit) {
+ return Result.ok(walletService.recentLedger(userId, limit));
+ }
+
+ @PostMapping("/coins")
+ @PreAuthorize("@ss.hasPermission('wallet:update')")
+ @OperationLog(module = "订单管理", action = "人工补币")
+ public Result adjust(@Valid @RequestBody CoinAdjustReqVO req) {
+ // 必须先确认用户存在:否则会凭空给一个不存在的 userId 建钱包,
+ // 留下对不上账的脏数据(自测时确实踩到了这个坑)。
+ if (appUserMapper.selectById(req.getUserId()) == null) {
+ throw new ServiceException(ErrorCode.DATA_NOT_FOUND, "用户不存在,无法补币");
+ }
+ String key = req.getIdempotentKey() == null || req.getIdempotentKey().isBlank()
+ ? "admin:" + System.currentTimeMillis() + ":" + req.getUserId()
+ : req.getIdempotentKey();
+ return Result.ok(walletService.change(req.getUserId(), req.getAmount(),
+ WalletService.BIZ_ADMIN, "", key,
+ req.getRemark() == null ? "人工调整" : req.getRemark(),
+ false, SecurityUtils.getUserId()));
+ }
+}
diff --git a/fruitshorts-app-api/pom.xml b/fruitshorts-app-api/pom.xml
index 24471f2..16659f5 100644
--- a/fruitshorts-app-api/pom.xml
+++ b/fruitshorts-app-api/pom.xml
@@ -35,6 +35,10 @@
com.fruitshorts
fruitshorts-module-interaction
+
+ com.fruitshorts
+ fruitshorts-module-commerce
+
diff --git a/fruitshorts-app-api/src/main/java/com/fruitshorts/app/controller/AppCommerceController.java b/fruitshorts-app-api/src/main/java/com/fruitshorts/app/controller/AppCommerceController.java
new file mode 100644
index 0000000..0d1df14
--- /dev/null
+++ b/fruitshorts-app-api/src/main/java/com/fruitshorts/app/controller/AppCommerceController.java
@@ -0,0 +1,63 @@
+package com.fruitshorts.app.controller;
+
+import com.fruitshorts.commerce.dal.entity.Orders;
+import com.fruitshorts.commerce.service.OrderService;
+import com.fruitshorts.commerce.service.UnlockService;
+import com.fruitshorts.commerce.service.WalletService;
+import com.fruitshorts.common.pojo.Result;
+import com.fruitshorts.framework.security.SecurityUtils;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 客户端金币与订阅。
+ *
+ * 金币解锁是第一阶段真实的收费方式;真实商店购买走 {@code /app/orders/verify},
+ * 本轮只预留接口(商店未接入时返回明确业务提示,不会产生假订单)。
+ */
+@RestController
+@RequestMapping("/app")
+@RequiredArgsConstructor
+public class AppCommerceController {
+
+ private final WalletService walletService;
+ private final UnlockService unlockService;
+ private final OrderService orderService;
+
+ /** 我的钱包:余额 + 最近流水,客户端一处渲染。 */
+ @GetMapping("/wallet")
+ public Result