qmj 4 дней назад
Родитель
Сommit
661c2bee9d

Разница между файлами не показана из-за своего большого размера
+ 17 - 0
.claude/homunculus/observations.jsonl


+ 2 - 2
.specify/feature.json

@@ -1,3 +1,3 @@
 {
-  "feature_directory": "specs/010-order-invoice"
-}
+  "feature_directory": "specs/016-omg-payment"
+}

+ 1 - 1
CLAUDE.md

@@ -172,5 +172,5 @@ Strong success criteria let you loop independently. Weak criteria ("make it work
 
 <!-- SPECKIT START -->
 For additional context about technologies to be used, project structure,
-shell commands, and other important information, read the current plan: `specs/013-food-spec/plan.md`
+shell commands, and other important information, read the current plan: `specs/016-omg-payment/plan.md`
 <!-- SPECKIT END -->

+ 19 - 6
ruoyi-admin/src/main/java/com/ruoyi/app/order/OrderInvoiceService.java

@@ -255,7 +255,14 @@ public class OrderInvoiceService {
         } else if (isPersonalChoice(row.getInvoiceChoice())) {
             InfoUser u = infoUserService.getOne(
                     new LambdaQueryWrapper<InfoUser>().eq(InfoUser::getUserId, order.getUserId()));
-            dto.setBuyerName(u != null && StrUtil.isNotBlank(u.getPhone()) ? u.getPhone() : "會員");
+            String phone = u != null && StrUtil.isNotBlank(u.getPhone()) ? u.getPhone() : "會員";
+            dto.setBuyerName(phone);
+            if ("MEMBER".equals(row.getInvoiceChoice())) {
+                // 会员载具:载具号=会员手机号;同步落库保持发票行载具信息完整
+                dto.setCarrierNum(phone);
+                row.setCarrierType("2");
+                row.setCarrierNum(phone);
+            }
         }
         // 出餐触发开票:在意图行上落结果,并标记已触发(与 invoiceStatus 独立)
         row.setInvoiceCategory(dto.getCategory());
@@ -278,6 +285,11 @@ public class OrderInvoiceService {
                 dto.setCarrierType(intent.getCarrierType()); // PAPER 时为 null → 个人纸本分支
                 dto.setCarrierNum(intent.getCarrierNum());
                 break;
+            case "MEMBER":
+                // 会员载具 ezPay 类型 2;载具号(会员手机号)与买方名由 autoIssue 取 InfoUser.phone 回填
+                dto.setCategory("B2C");
+                dto.setCarrierType("2");
+                break;
             case "LOVE_CODE":
                 dto.setCategory("DONATION");
                 dto.setLoveCode(intent.getLoveCode());
@@ -295,7 +307,8 @@ public class OrderInvoiceService {
     }
 
     private boolean isPersonalChoice(String choice) {
-        return "PAPER".equals(choice) || "PHONE_BARCODE".equals(choice) || "CITIZEN".equals(choice);
+        return "PAPER".equals(choice) || "PHONE_BARCODE".equals(choice) || "CITIZEN".equals(choice)
+                || "MEMBER".equals(choice);
     }
 
     /** 客户查询订单发票;门店不可开票时 invoiceStatus=null(客户端据此隐藏入口)。 */
@@ -581,6 +594,9 @@ public class OrderInvoiceService {
                     throw new ServiceException("自然人凭证格式不正确(须为 2 位大写英文 + 14 位数字)");
                 }
                 break;
+            case "MEMBER":
+                // 会员载具:载具号=会员手机号(出餐 autoIssue 时取 InfoUser.phone),免联网验真、免格式校验
+                break;
             case "LOVE_CODE":
                 if (StrUtil.isBlank(loveCode) || !loveCode.matches("^\\d{3,7}$")) {
                     throw new ServiceException("捐赠码格式不正确(须为 3-7 位数字)");
@@ -636,7 +652,7 @@ public class OrderInvoiceService {
 
     /**
      * 场景入参校验(service 内强校验,拦截非法请求不调 ezPay)。对齐 ezPay 开票模型:
-     * B2B:统编(8位)+邮箱必填、无载具;B2C:载具(0/1/2)+号码必填,ezPay会员载具(2)时邮箱必填
+     * B2B:统编(8位)+邮箱必填、无载具;B2C:载具(0/1/2)+号码必填;会员载具(2)载具号=会员手机号、不收邮箱
      */
     private void validateInvoiceInput(ApplyInvoiceDto dto) {
         if ("B2B".equals(dto.getCategory())) {
@@ -697,9 +713,6 @@ public class OrderInvoiceService {
                 default:
                     break;
             }
-            if ("2".equals(ct) && StrUtil.isBlank(dto.getBuyerEmail())) {
-                throw new ServiceException("ezPay 会员载具须填写买方邮箱");
-            }
         }
     }
 

+ 4 - 2
ruoyi-admin/src/main/java/com/ruoyi/app/order/UserOrderController.java

@@ -312,6 +312,8 @@ public class UserOrderController extends BaseController {
                     intent.setCarrierType("0");
                 } else if ("CITIZEN".equals(choice)) {
                     intent.setCarrierType("1");
+                } else if ("MEMBER".equals(choice)) {
+                    intent.setCarrierType("2");
                 } else {
                     intent.setCarrierType(input.getCarrierType());
                 }
@@ -354,8 +356,8 @@ public class UserOrderController extends BaseController {
     //保存默认发票抬头(010 Phase 11:结账勾「同意设置为默认」;同一用户仅一条 is_default=1)
     private void saveDefaultInvoiceProfile(OrderCreateInput input, Long userId) {
         String choice = input.getInvoiceChoice();
-        if ("PAPER".equals(choice) || "LOVE_CODE".equals(choice)) {
-            return; // 纸本/捐赠不存抬头
+        if ("PAPER".equals(choice) || "LOVE_CODE".equals(choice) || "MEMBER".equals(choice)) {
+            return; // 纸本/捐赠/会员载具不存抬头(会员载具=会员手机号,无需记忆)
         }
         InfoInvoice profile = new InfoInvoice();
         profile.setUserId(userId);

+ 1 - 1
ruoyi-admin/src/main/java/com/ruoyi/app/order/dto/OrderCreateInput.java

@@ -48,7 +48,7 @@ public class OrderCreateInput {
     private String reservePhone;
 
     /** === 发票意图(下单时选,出餐自动开票;010 Phase 11)=== */
-    /** 发票选择 PAPER个人纸本(默认)/PHONE_BARCODE手机条码/CITIZEN自然人凭证/LOVE_CODE捐赠/COMPANY公司 */
+    /** 发票选择 PAPER个人纸本(默认)/PHONE_BARCODE手机条码/CITIZEN自然人凭证/MEMBER会员载具/LOVE_CODE捐赠/COMPANY公司 */
     private String invoiceChoice;
     /** 载具类型 0手机条码/1自然人凭证(PHONE_BARCODE/CITIZEN 时填) */
     private String carrierType;

+ 6 - 6
ruoyi-admin/src/main/java/com/ruoyi/app/pay/UserBillingController.java

@@ -358,18 +358,18 @@ public class UserBillingController extends BaseController {
     private void setShList(List<UserBilling> list, JSONObject org, InfoUser user) {
         List<ShBillingDto> result = new ArrayList<>();
         if (!list.isEmpty()) {
-            List<Long> ddIds = list.stream().map(x -> Long.valueOf(x.getDdId())).collect(Collectors.toList());
+            List<String> ddIds = list.stream().map(x -> x.getDdId()).collect(Collectors.toList());
             List<PosOrder> orders = posOrderService.list(new LambdaQueryWrapper<PosOrder>().in(PosOrder::getDdId, ddIds));
             List<PosOrderRating> ratings = posOrderRatingService.list(new LambdaQueryWrapper<PosOrderRating>().in(PosOrderRating::getDdId, ddIds));
             list.forEach(x -> {
                 ShBillingDto dto = Convert.convert(ShBillingDto.class, x);
                 //货到付款的订单金额
-                orders.stream().filter(o -> o.getDdId().equals(Long.valueOf(x.getDdId()))).findFirst().ifPresent(o -> {
+                orders.stream().filter(o -> o.getDdId().equals(x.getDdId())).findFirst().ifPresent(o -> {
                     dto.setFoodAmount(o.getFoodAmount());
                     int shYh = (o.getMdDiscountAmount() == null ? 0 : o.getMdDiscountAmount()) + (o.getMdSalesReduction() == null ? 0 : o.getMdSalesReduction());
                     dto.setShYh((double) shYh);
                 });
-                ratings.stream().filter(r -> r.getDdId().equals(Long.valueOf(x.getDdId()))).findFirst().ifPresent(r -> dto.setShStarts(r.getQsStars()));
+                ratings.stream().filter(r -> r.getDdId().equals(x.getDdId())).findFirst().ifPresent(r -> dto.setShStarts(r.getQsStars()));
                 result.add(dto);
             });
         }
@@ -383,20 +383,20 @@ public class UserBillingController extends BaseController {
     private void setQsList(List<UserBilling> list, JSONObject org) {
         List<QsBillingDto> result = new ArrayList<>();
         if (!list.isEmpty()) {
-            List<Long> ddIds = list.stream().map(x -> Long.valueOf(x.getDdId())).collect(Collectors.toList());
+            List<String> ddIds = list.stream().map(x -> x.getDdId()).collect(Collectors.toList());
             List<PosOrder> orders = posOrderService.list(new LambdaQueryWrapper<PosOrder>().in(PosOrder::getDdId, ddIds));
             List<PosOrderRating> ratings = posOrderRatingService.list(new LambdaQueryWrapper<PosOrderRating>().in(PosOrderRating::getDdId, ddIds));
             list.forEach(x -> {
                 QsBillingDto dto = Convert.convert(QsBillingDto.class, x);
                 //货到付款的订单金额
-                orders.stream().filter(o -> o.getDdId().equals(Long.valueOf(x.getDdId()))).findFirst().ifPresent(o -> {
+                orders.stream().filter(o -> o.getDdId().equals(x.getDdId())).findFirst().ifPresent(o -> {
                     if ("1".equals(o.getCollectPayment())) {
                         dto.setOrderAmount(o.getAmount());
                     }
                     dto.setFreight(o.getFreight());
                     dto.setJvli(o.getJvli());
                 });
-                ratings.stream().filter(r -> r.getDdId().equals(Long.valueOf(x.getDdId()))).findFirst().ifPresent(r -> dto.setQsStarts(r.getQsStars()));
+                ratings.stream().filter(r -> r.getDdId().equals(x.getDdId())).findFirst().ifPresent(r -> dto.setQsStarts(r.getQsStars()));
                 result.add(dto);
             });
         }

+ 1 - 1
ruoyi-admin/src/main/java/com/ruoyi/app/pay/WalletController.java

@@ -85,7 +85,7 @@ public class WalletController extends BaseController {
         IPage<UserBilling> result = userBillingService.page(pageInput, queryWrapper);
 
         if (!result.getRecords().isEmpty()) {
-            List<Long> orderDdIds = result.getRecords().stream().filter(x -> x.getType().equals("0")).map(x -> Long.parseLong(x.getDdId())).collect(Collectors.toList());
+            List<String> orderDdIds = result.getRecords().stream().filter(x -> x.getType().equals("0")).map(x -> x.getDdId()).collect(Collectors.toList());
             List<String> withDrawalDdIds = result.getRecords().stream().filter(x -> x.getType().equals("2")).map(UserBilling::getDdId).collect(Collectors.toList());
             List<String> rechargeDdIds = result.getRecords().stream().filter(x -> x.getType().equals("1")).map(UserBilling::getDdId).collect(Collectors.toList());
 

+ 1 - 1
ruoyi-system/src/main/java/com/ruoyi/system/domain/PosOrderInvoice.java

@@ -37,7 +37,7 @@ public class PosOrderInvoice {
     /** 冗余 pos_store.id(门店) */
     private Long storeId;
 
-    /** 发票选择(下单意图) PAPER个人纸本/PHONE_BARCODE手机条码/CITIZEN自然人凭证/LOVE_CODE捐赠/COMPANY公司 */
+    /** 发票选择(下单意图) PAPER个人纸本/PHONE_BARCODE手机条码/CITIZEN自然人凭证/MEMBER会员载具/LOVE_CODE捐赠/COMPANY公司 */
     private String invoiceChoice;
 
     /** 发票类型 B2C/B2B */

+ 1 - 1
ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/ApplyInvoiceDto.java

@@ -31,7 +31,7 @@ public class ApplyInvoiceDto {
     @Pattern(regexp = "\\d{8}", message = "统一编号必须为 8 位数字")
     private String buyerUbn;
 
-    /** 买方邮箱(B2B 必填;B2C 选 ezPay会员载具(2)时必填,其余可空) */
+    /** 买方邮箱(B2B 必填;B2C 选 ezPay会员载具(2)时不收、其余可空) */
     @Email(message = "邮箱格式不正确")
     private String buyerEmail;
 

+ 37 - 0
specs/015-intl-flight/checklists/requirements.md

@@ -0,0 +1,37 @@
+# Specification Quality Checklist: 国际机票(盘合 iFlight 分销接入)
+
+**Purpose**: Validate specification completeness and quality before proceeding to planning
+**Created**: 2026-07-29
+**Feature**: [spec.md](../spec.md)
+
+## Content Quality
+
+- [x] No implementation details (languages, frameworks, APIs) —— 保持在业务级
+- [x] Focused on user value and business needs
+- [x] Written for non-technical stakeholders
+- [x] All mandatory sections completed
+
+## Requirement Completeness
+
+- [ ] No [NEEDS CLARIFICATION] markers remain —— 当前保留 FR-009~FR-012(D1/D2/D4/D5),待用户拍板
+- [x] Requirements are testable and unambiguous(已定部分可测)
+- [x] Success criteria are measurable
+- [x] Success criteria are technology-agnostic
+- [ ] All acceptance scenarios are defined —— 范围未定(D1),仅按「假设全流程」写
+- [ ] Edge cases are identified —— 列了主要项,待范围确认后补充
+- [ ] Scope is clearly bounded —— 待 D1
+- [x] Dependencies and assumptions identified
+
+## Feature Readiness
+
+- [ ] All functional requirements have clear acceptance criteria —— D1/D2/D4/D5 待定
+- [x] User scenarios cover primary flows(按假设全流程覆盖)
+- [ ] Feature meets measurable outcomes defined in Success Criteria —— 待实现后验证
+- [x] No implementation details leak into specification
+
+## Notes
+
+- 本 spec 当前为**业务级 Draft**,核心价值在于记录已确认的集成背景与「两层资金流」资金模型。
+- **下一步阻塞在「待定决策」D1–D6**,尤其是 **D3(iFlight 接口契约获取)是硬阻塞**——未拿到文档前 `/speckit-plan` 的 research 与 `/speckit-tasks` 无法进入字段级实现。
+- 用户明确表示「后续的我再决定」,故暂保留 [NEEDS CLARIFICATION] 标记,不强行猜测。
+- 待用户决策后,先跑 `/speckit-clarify` 消化决策、再 `/speckit-plan`。

+ 154 - 0
specs/015-intl-flight/spec.md

@@ -0,0 +1,154 @@
+# Feature Specification: 国际机票(盘合 iFlight 分销接入)
+
+**Feature Branch**: `(待创建,暂未建分支/未提交)`
+
+**Created**: 2026-07-29
+
+**Status**: Draft —— 范围与多项关键决策待定(见「待定决策」)
+
+**Input**: 接入上海盘合 shpanhe 国际机票开放接口(`http://open.shpanhe.com/api/iFlight`),实现国际机票查询/预订等能力。凭证见 `文档/旅游/API测试接口.txt`。
+
+---
+
+## 集成背景(已确认事实)
+
+- **盘合是分销平台,我方是分销商**:从同平台国内机票文档出现的「票面价 / 机建税 / 燃油费 / 结算价 / 返点返现政策」可确认这是 **B2B 分销结算模型**,不是消费者直连航司/盘合的透传支付。
+- **测试凭证(国际机票)**:
+  - AppKey:`4298c05600816da82abdbc899cd0c344`
+  - SecretKey:`7ad3915eaf9a47156adcd31a1c51f2df`
+  - 接口网关域名:`http://api.panhe.net`
+  - 测试分销后台:`http://fxtest.panhe.net`(账号 `15978194868` / 密码 `086915`)
+- **接口文档可获取性**:iFlight 在线文档(`open.shpanhe.com/api/iFlight`)需登录,自动抓取(HTTP/HTTPS/真实浏览器)均被屏蔽或超时失败,搜索引擎未收录。同平台**国内机票文档 `/api/flight` 公开**,可作为流程/签名/信封惯例的参考模板(国际机票大概率同源,待文档验证)。
+
+### 资金模型:两层资金流(核心)
+
+| 层级 | 方向 | 价格 | 通道 |
+|------|------|------|------|
+| 第一层(零售) | 消费者 → 平台 | 零售价(票面价 + 税费 + 我方加价/返点) | 平台自有支付通道(复用,见下) |
+| 第二层(结算) | 平台 → 盘合 | 结算价 | 分销商账户(余额/授信/代扣,方式待文档确认) |
+
+- 消费者全程不接触盘合。
+- **结算价与零售价的差 / 返点返现 = 平台利润**。
+- 第一层收款**复用 foodie 现有支付通道**,不为机票新建支付:当前 NewebPay 蓝新金流**代码就绪但未启用**,VNPay / ZaloPay 已废弃(详见 `project-deprecated-code` 记忆)。
+- 真正新增的复杂点不是支付通道本身,而是:**零售价定价**、**结算价/利润记账**、**出票后资金核销与对账**。
+
+---
+
+## 待定决策(用户后续决定)
+
+> 以下决策显著影响 spec 范围与用户场景,需用户拍板后再细化。当前 spec 先以「假设全流程」记录,范围边界以 D1 为准回收。
+
+- **D1 — 范围边界**:仅查票展示 / 查票+生单(不含支付出票)/ 全流程(查→订→付→出票→售后)。
+- **D2 — 落地端**:用户端(uni-app,消费者自助)/ 平台管理端(foodie-admin-vue)/ 两端。
+- **D3 — iFlight 接口契约获取方式**:用户提供完整文档 / 以国内机票 `/api/flight` 为模板推断(后续校正)/ 仅写业务级 spec、接口细节推迟到 plan 的 research 步骤。
+- **D4 — 第二层(平台→盘合)结算方式**:账户余额实时扣款 / 授信额度周期结算 / 下单网关代扣。(决定记账与资金风险设计)
+- **D5 — 零售价定价策略**:直接用票面价 / 票面价 + 固定加价 / 按返点反算。(决定利润模型)
+- **D6 — 第一层(消费者)收款通道**:是否启用 NewebPay 或使用当时可用通道。
+
+> ⚠️ **硬阻塞**:在拿到 iFlight 接口契约(D3)前,`/speckit-plan` 的 research 与 `/speckit-tasks` 的实现细节无法落地,本 spec 只能停留在业务级。
+
+---
+
+## User Scenarios & Testing *(mandatory)*
+
+> 优先级以「假设全流程(D1=全流程)」编写;若 D1 收窄为「仅查票」,则仅保留 US1,其余降级或移除。
+
+### User Story 1 - 查询国际航班(Priority: P1)
+
+消费者选择出发/到达城市(三字码)、出发日期、舱等,查看可选国际航班及其舱位票价(票面价、税费、飞行时段、航司、中转信息)。
+
+**Why this priority**:查票是整个机票功能的入口和最小可用形态,即使后续环节都不做,单查票展示也有独立价值。
+
+**Independent Test**:给定一条真实国际航线 + 日期,调用查询能返回航班与价格列表并在前端展示。
+
+**Acceptance Scenarios**:
+
+1. **Given** 消费者输入有效的出发地、到达地、日期,**When** 发起查询,**Then** 返回当日可选航班列表(含航司、起降时间、舱位、票价、税费、是否中转)。
+2. **Given** 无效或无航班航线,**When** 查询,**Then** 返回明确的「无航班」提示而非报错。
+3. **Given** 查询往返/联程,**When** 查询,**Then** 通过分段查询组合出往返/联程结果。
+
+---
+
+### User Story 2 - 下单与支付(Priority: P2)
+
+消费者选定航班舱位、填写乘机人信息后生成订单,按零售价通过平台支付通道付款。
+
+**Why this priority**:完成「能买到票」的核心交易闭环。
+
+**Independent Test**:从选定航班到支付成功,生成一条待出票的机票订单。
+
+**Acceptance Scenarios**:
+
+1. **Given** 消费者选定舱位并填妥乘机人,**When** 提交订单,**Then** 生成机票订单并按零售价发起支付。
+2. **Given** 支付成功,**When** 平台收到回调,**Then** 订单进入「待出票」,平台据此向盘合按结算价下单/订座。
+3. **Given** 支付超时或失败,**When** 超时未支付,**Then** 订单自动取消、释放占座(若已占座)。
+
+---
+
+### User Story 3 - 出票与售后(Priority: P3)
+
+支付成功后由盘合出票,消费者获得票号;支持退票/改签等售后。
+
+**Why this priority**:完成完整购票体验,但可晚于 MVP。
+
+**Independent Test**:支付成功后能拿到真实票号并展示;能发起一次退票流程。
+
+**Acceptance Scenarios**:
+
+1. **Given** 订单已支付且盘合侧下单成功,**When** 出票完成,**Then** 消费者可见票号/行程单。
+2. **Given** 出票失败,**When** 平台感知失败,**Then** 自动重试或退款,并通知消费者。
+3. **Given** 已出票订单,**When** 消费者申请退票/改签,**Then** 按航司规则计算手续费并退款/改期。
+
+---
+
+### Edge Cases
+
+- 查票有结果但下单时座位已被占满(库存瞬时变化)如何处理?
+- 支付成功但盘合侧出票失败的资金兜底(退款给消费者 + 平台与盘合的结算对账)。
+- 价格在查票与下单之间发生变动(税费/汇率波动)。
+- 多乘机人部分出票成功部分失败。
+- 国际行程的护照/证件信息校验、签证提示(信息性,非校验)。
+- 币种与汇率:盘合结算币种 vs 消费者支付币种。
+
+## Requirements *(mandatory)*
+
+### Functional Requirements
+
+- **FR-001**:系统 MUST 支持按出发/到达城市(三字码)、日期、舱等查询国际航班,返回航班、舱位、票面价、税费、起降时间、航司、中转信息。
+- **FR-002**:系统 MUST 支持往返/联程查询(分段组合实现)。
+- **FR-003**:系统 MUST 在消费者选定舱位并填写乘机人后生成国际机票订单,订单含零售价与结算价两套价格记录。
+- **FR-004**:系统 MUST 通过平台自有支付通道收取消费者零售价款项(复用,不新建支付通道)。
+- **FR-005**:系统 MUST 在消费者支付成功后,按结算价向盘合完成下单/订座/出票,并记录票号。
+- **FR-006**:系统 MUST 记录每笔订单的两层资金流(消费者零售收款 + 平台与盘合结算价/返点),支持对账与利润核算。
+- **FR-007**:系统 MUST 支持出票失败的自动重试与退款兜底。
+- **FR-008**:系统 MUST 支持退票/改签,按航司规则计算手续费。
+- **FR-009**:功能范围(查票/下单/支付/出票/售后)以 **[NEEDS CLARIFICATION: D1 范围边界]** 为准。
+- **FR-010**:功能落地端(用户端/管理端)以 **[NEEDS CLARIFICATION: D2 落地端]** 为准。
+- **FR-011**:零售价定价以 **[NEEDS CLARIFICATION: D5 定价策略]** 为准。
+- **FR-012**:第二层结算方式以 **[NEEDS CLARIFICATION: D4 结算方式]** 为准。
+
+### Key Entities *(include if feature involves data)*
+
+- **航班查询结果(FlightSearchResult)**:航班段、舱位、票面价、税费、结算价、返点政策、起降时间、航司、中转。
+- **国际机票订单(IntlFlightOrder)**:订单号、消费者、航线、舱位、乘机人列表、零售价、结算价、状态(待支付/待出票/已出票/已退/已改)、关联支付流水、票号。
+- **乘机人(Passenger)**:姓名(拼音/英文)、证件类型与号码(护照等)、性别、出生日期、手机号。
+- **资金流水(FlightPaymentSettlement)**:消费者侧收款记录 + 平台与盘合的结算扣款/返点记录(用于对账与利润)。
+
+## Success Criteria *(mandatory)*
+
+### Measurable Outcomes
+
+- **SC-001**:消费者发起国际航班查询后,95% 的查询在可接受时长内返回结果。
+- **SC-002**:查票成功率(有航线的查询返回有效结果的比例)达到目标值。
+- **SC-003**:从下单到出票完成(含支付)的端到端时长满足消费者预期。
+- **SC-004**:每笔订单的两层资金(零售收款 vs 盘合结算)100% 可对账,利润可核算。
+- **SC-005**:出票失败场景 100% 有明确的退款/重试兜底,无资金悬空。
+
+## Assumptions
+
+- 平台已作为**分销商**在盘合完成开户,测试凭证可用,生产凭证后续提供。
+- 复用 foodie 现有的**用户体系、App 端框架、支付通道**,不为此功能新建用户体系或支付通道。
+- 国际机票与国内机票同源(同一平台),**签名方式、请求信封、接口流程一致**——此为推断,待 iFlight 文档验证。
+- 在拿到 iFlight 接口契约前,`plan`/`tasks`/`实现` 阶段无法进入字段级细节,本 spec 保持业务级。
+- 接口调用走服务端(AppKey/SecretKey 不下发前端),签名在服务端完成。
+- 测试先用 `api.panhe.net` 测试网关与测试凭证,生产环境与凭证后续切换。

+ 36 - 0
specs/016-omg-payment/checklists/requirements.md

@@ -0,0 +1,36 @@
+# Specification Quality Checklist: OMG 支付接入(订单在线支付)
+
+**Purpose**: Validate specification completeness and quality before proceeding to planning
+**Created**: 2026-07-29
+**Feature**: [spec.md](../spec.md)
+
+## Content Quality
+
+- [x] No implementation details (保持在业务+集成事实层,未写代码/算法细节)
+- [x] Focused on user value and business needs
+- [x] Written for non-technical stakeholders
+- [x] All mandatory sections completed
+
+## Requirement Completeness
+
+- [x] No [NEEDS CLARIFICATION] markers remain —— D1–D6 已定,FR-002/007/009/012 已落实
+- [x] Requirements are testable and unambiguous
+- [x] Success criteria are measurable
+- [x] Success criteria are technology-agnostic
+- [x] All acceptance scenarios are defined(5 个用户故事覆盖)
+- [x] Edge cases are identified
+- [x] Scope is clearly bounded —— D1(全方式)/D2(替代NewebPay)/D5(仅餐饮)/D6(门店级) 已定;D3 退款本期、D4 LINE Pay 不做
+- [x] Dependencies and assumptions identified
+
+## Feature Readiness
+
+- [x] All functional requirements have clear acceptance criteria
+- [x] User scenarios cover primary flows
+- [ ] Feature meets measurable outcomes defined in Success Criteria —— 待实现后验证
+- [x] No implementation details leak into specification
+
+## Notes
+
+- 决策 D1–D6 已确认并写入 spec(2026-07-29)。范围:OMG 全方式支付(ALL,不含 LINE Pay)、替代蓝新 NewebPay、仅餐饮订单、门店级凭证、本期含退款。
+- 核心集成结论:OMG `CheckMacValue`(SHA256) 签名与 NewebPay(TradeInfo/AES) 不同,需独立签名工具。
+- 下一步:`/speckit-plan`(research 须补齐 CheckMacValue 算法细节、05_refund/04_order_query/03_payment_notify 字段、门店级凭证表设计)。

+ 120 - 0
specs/016-omg-payment/contracts/api.md

@@ -0,0 +1,120 @@
+# Contracts: OMG(歐買尬/FunPoint)AIO 支付接入
+
+**Feature**: specs/016-omg-payment/spec.md
+**Date**: 2026-07-29
+
+> **约束**:OMG 全部独立实现,零 `newebpay` 依赖(见 research.md §0)。签名统一用 `CheckMacValue`(SHA256, EncryptType=1),无 AES、无解密。
+
+---
+
+## A. OMG 外部接口(我方 → 盘合)
+
+### A1. 幕前下单 — `Cashier/AioCheckOut/V5`(US1)
+
+- **URL**:测试 `https://payment-stage.funpoint.com.tw/Cashier/AioCheckOut/V5`;正式 `https://payment.funpoint.com.tw/Cashier/AioCheckOut/V5`
+- **Method/CT**:`POST`,`application/x-www-form-urlencoded`
+- **方式**:后端组参 + 计算 CheckMacValue 返回 form 字段,**前端 Form Post** 跳转 OMG 收银台(不可 iframe;iOS 不可另开窗)
+- **必填请求参数**:`MerchantID`、`MerchantTradeNo`(=OMG+ddId)、`MerchantTradeDate`(yyyy/MM/dd HH:mm:ss)、`PaymentType=aio`、`TotalAmount`(整数元)、`ReturnURL`、`ChoosePayment=ALL`、`CheckMacValue`、`EncryptType=1`
+- **常用可选**:`TradeDesc`、`ItemName`(# 分隔)、`OrderResultURL`、`PaymentInfoURL`、`ClientRedirectURL`、`NeedExtraPaidInfo`(建议 Y,供退款查 gwsr)、`CustomField1–4`、`Language`
+- **固定**:`InvoiceMark=N`(发票走 ezPay);`ReturnURL ≠ OrderResultURL`;`MerchantTradeNo` 全平台唯一
+
+### A2. 付款结果回调 — ReturnURL(盘合 → 我方,US2)
+
+- 盘合**服务端 POST** 我方 `/pay/omg/notify`(CT `text/html`)
+- **参数**:`MerchantID`、`MerchantTradeNo`、`StoreID`、`RtnCode`(**1=成功**,其余勿发货)、`RtnMsg`、`TradeNo`(OMG 交易号,须存并与 MerchantTradeNo 关联)、`TradeAmt`、`PaymentDate`、`PaymentType`、`TradeDate`、`SimulatePaid`(**1=模拟,勿发货**)、`CustomField1–4`、`CheckMacValue`
+- **我方响应**:纯字符串 `1|OK`(首字符 1=成功);未收到 5–15 分重试,当天最多 4 次
+- **核销校验顺序**:CheckMacValue 验签 → `RtnCode==1` → `SimulatePaid!=1` → `TradeAmt==订单 amount` → `trade_no` 幂等
+
+### A3. ATM/超商 取号回调 — PaymentInfoURL(US3)
+
+- 盘合服务端 POST 我方 `/pay/omg/paymentInfo`,返回虚帐/缴费码(`BankCode`/`vAccount`/`ExpireDate` 或 `PaymentNo`/`ExpireDate` 等,详见文档 02 页),带 `CheckMacValue` 需验签
+- 我方落库展示并回 `1|OK`
+
+### A4. 订单查询 — `Cashier/QueryTradeInfo/V5`(凭证验证 / 补单)
+
+- **URL**:测试 `https://payment-stage.funpoint.com.tw/Cashier/QueryTradeInfo/V5`;正式 `https://payment.funpoint.com.tw/Cashier/QueryTradeInfo/V5`
+- **请求**:`MerchantID`、`MerchantTradeNo`、`TimeStamp`(3 分钟内)、`PlatformID?`、`CheckMacValue`
+- **响应**(text/html,k=v):`TradeStatus`(**0 未付/1 已付/10200095 失败**)、`TradeNo`、`TradeAmt`、`PaymentDate`、`PaymentType`、`TradeDate`、`CheckMacValue`…
+- **用途**:录入凭证探测金钥 + 回调漏收时补单
+
+### A5. 信用卡退款/取消 — `CreditDetail/DoAction`(US4)
+
+- **URL**:正式 `https://payment.funpoint.com.tw/CreditDetail/DoAction`(**stage 不可用**)
+- **请求**:`MerchantID`、`MerchantTradeNo`、`TradeNo`、`Action`(**C** 關帳/**R** 退刷/**E** 取消/**N** 放棄)、`TotalAmount`、`CheckMacValue`、`PlatformID?`
+- **响应**(k=v):`RtnCode`(**1=成功**)、`RtnMsg`
+- **规则**:`已關帳→R`、`已授權→N`、`要關帳→E 再 N 或 R`;分期必须全额;ATM/CVS/BarcodeATM **不适用**(走人工);自动关帳开启时避开 20:15–20:30
+
+---
+
+## B. 平台内部接口(暴露给前端/管理端)
+
+### B1. 发起 OMG 支付 — `POST /pay/omg/create`(US1,@Anonymous @Auth,Header token)
+
+- 入参:`orderid`(= ddId)
+- 逻辑:校验订单归属/金额/未支付 → 查门店启用凭证 → 生成 `MerchantTradeNo` → 组参 + CheckMacValue → 落 `pos_order_omg_payment`(pay_status=0) + 更新 `pos_order.pay_type="7"`/`pay_url`
+- 返回:form 字段 `{ gatewayUrl, MerchantID, MerchantTradeNo, MerchantTradeDate, PaymentType=aio, TotalAmount, ReturnURL, ChoosePayment=ALL, EncryptType=1, ItemName, CheckMacValue, ... }`,前端构建隐藏 form 自动 submit 到 `gatewayUrl`
+
+### B2. OMG 支付结果回调 — `POST /pay/omg/notify`(US2,@Anonymous)
+
+- 处理 A2:collectForm → IpnLog → 凭证(MerchantID) → 验签 CheckMacValue → 幂等(trade_no) → 金额校验 → RtnCode==1 && SimulatePaid!=1 → markSuccess → 更新订单(state=0, payStatus=1) + 订单日志 + 推送用户/商家/骑手 + sendAcceptRiderPush → 回纯串 `1|OK`
+
+### B3. 支付完成返回页 — `GET|POST /pay/omg/return`(US2,@Anonymous)
+
+- 仅引导回前端结果页(带 ddId),**不改订单状态**(以 notify 为准)
+
+### B4. ATM/超商 取号回调 — `POST /pay/omg/paymentInfo`(US3,@Anonymous)
+
+- 处理 A3:验签 → 落虚帐/缴费码 + 期限 → 回 `1|OK`
+
+### B5. 订单查询/补单 — `POST /pay/omg/query`(US2 补单,可选)
+
+- 入参 `orderid` → 调 A4 → 据 TradeStatus 补单/查状态
+
+### B6. 订单取消退款(US4)
+
+- 触发点:订单取消链路(`PosOrderShOprate` 商家取消 / `UserOrderController` 用户取消)内,对 OMG 已支付订单调 A5 DoAction;或 `POST /pay/omg/refund?orderid=`(落点 tasks 定)
+- 写 `pos_order_omg_refund`;成功置 `pos_order_omg_payment.pay_status=3`、`pos_order.pay_status=2`
+
+### B7. 门店 OMG 凭证管理 — `/system/storeOmg/*`(US5,@PreAuthorize)
+
+- `GET /list`(分页筛选)、`GET /{storeId}`、`PUT /apply/{storeId}`、`PUT /saveCredentials`(录入+QueryTradeInfo 探测验证)、`PUT /toggleEnable/{storeId}`、`PUT /reset/{storeId}`、`PUT /enabledPayments/{storeId}`
+- 镜像 011 `PosStoreNewebpayController` 模式,但**独立 Controller/Service/实体/表**
+
+---
+
+## C. 代码表与枚举(OMG reference 02–05,2026-07-29 补读)
+
+### C1. ChoosePayment(请求参数,US1)
+
+本期固定 `ALL`(聚合全部)。可细分值:`Credit`(信用卡/銀聯/Apple Pay)、`ATM`(FIRST/CHINATRUST/UBOT/KGI)、`CVS`(CVS/FAMILY/IBON/HILIFE)、`BarcodeATM`(CHINATRUST)、`AFTEE`。
+⚠️ **Apple Pay 不是独立 ChoosePayment**,归在 `Credit` 下(付款页内选);回覆 PaymentType 同为 `Credit_CreditCard`,**仅凭 PaymentType 无法区分信用卡与 Apple Pay**(本期不区分)。
+
+### C2. 回覆 PaymentType(回调 `PaymentType`,落 `pos_order_omg_payment.pay_type`)
+
+| PaymentType | 名称 | 即时/延期 |
+|---|---|---|
+| `Credit_CreditCard` | 信用卡(含銀聯/Apple Pay) | 即时 |
+| `BarcodeATM_CHINATRUST` | 超商快付代碼繳費 | 即时 |
+| `ATM_FIRST` / `ATM_CHINATRUST` / `ATM_UBOT` / `ATM_KGI` | 各银行 ATM | 延期(取虚帐→期限内付款) |
+| `CVS_CVS` / `CVS_FAMILY` / `CVS_IBON` / `CVS_HILIFE` | 超商代碼繳費 | 延期 |
+| `AFTEE_AFTEE` | AFTEE 先享後付 | 延期 |
+
+延期方式(ATM/CVS/AFTEE)走 `PaymentInfoURL` 取号 → US3。
+
+### C3. RtnCode(回调 / 退款响应)
+
+`1` = 成功;其余均为失败。完整代码表文档为**图片**且持续新增,须到「歐買尬廠商後台 → 系統開發管理 → 交易狀態代碼查詢」查全。
+**实现策略**:`RtnCode==1` 视成功(核销 / 退款成功);非 1 一律失败,记录 `rtn_code`+`rtn_msg` 供诊断,**不硬编码错误码分支**。
+
+### C4. URLEncode(CheckMacValue 计算,须完全照此 · reference 05)
+
+.NET 风格 URLEncode。**Java 实现**:`URLEncoder.encode(s, UTF_8)`(空格已→`+`)后,照 PHP 范例做替换以对齐 .NET,再转小写、SHA256、hex 大写:
+- 不编码(字面):`-` `_` `.` `!` `*` `(` `)`;`~`→`%7e`(两侧同)。
+- 空格→`+`(.NET/Java 表单编码一致;标准 URLEncode 是 `%20`,勿混用)。
+- 其余特殊符 `@ # $ % ^ & = + ; ? / \ > < [ ] { } : ' " , |` 均为 `%XX`;中文按 UTF-8 `%XX`。
+- 替换集(防御性,照 PHP 范例):`%2d→-`、`%5f→_`、`%2e→.`、`%21→!`、`%2a→*`、`%28→(`、`%29→)`(Java `URLEncoder` 本就不编 `-_.`,`!*()` 需替换回)。
+
+## 签名约定(所有请求/回调通用)
+
+`CheckMacValue`(EncryptType=1, SHA256):
+去 CheckMacValue → 参数按 key 字母序 → `k=v&...` → 包夹 `HashKey={k}&..&HashIV={iv}` → .NET 风格 URL 编码(`-_.!*()` 不编码) → 转小写 → SHA256 → hex 大写。校验同理重算比对。实现:`OmgCheckMacValue`(独立,不复用 `NewebPayEncryptUtil`)。

+ 186 - 0
specs/016-omg-payment/data-model.md

@@ -0,0 +1,186 @@
+# Data Model: OMG(歐買尬/FunPoint)AIO 支付接入
+
+**Feature**: specs/016-omg-payment/spec.md
+**Date**: 2026-07-29
+
+本文定义新增实体与建表 DDL(DDL 同步写入 `updatesql/sql.md`,不直接执行)。
+
+> **约束**:OMG 全部独立实现,**不复用蓝新建的 `pos_store_newebpay` / `pos_order_payment`**(见 research.md §0)。故凭证表、流水表、退款表均为 OMG 专属新建。
+
+---
+
+## 新增实体
+
+### 1. PosStoreOmg(门店 OMG 凭证)— `pos_store_omg`
+
+与 `pos_store` 一对一。记录门店 OMG 申请状态、启用开关、凭证、启用支付方式。状态机沿用 009/011 模式(0 未申请 / 1 申请中 / 2 已开通 + is_enabled)。
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | BIGINT PK AUTO | 主键 |
+| store_id | BIGINT UK | 关联 pos_store.id(门店),唯一 |
+| omg_status | INT | 申请状态: 0 未申请/1 申请中/2 已开通 |
+| is_enabled | INT | 启用开关: 0 停用/1 启用(仅 status=2 有意义) |
+| merchant_id | VARCHAR(10) | OMG 商店代号 MerchantID |
+| hash_key | VARCHAR(64) | OMG HashKey |
+| hash_iv | VARCHAR(64) | OMG HashIV |
+| enabled_payments | VARCHAR(50) | 启用支付方式(本期固定 ALL,字段保留) |
+| apply_time | DATETIME | 提交申请时间(0→1) |
+| approved_time | DATETIME | 开通时间(→2) |
+| last_verify_result | VARCHAR(240) | 最近一次凭证验证结果 |
+| remark | VARCHAR(255) | 备注 |
+| create_time / update_time | DATETIME | 审计时间 |
+| create_by / update_by | VARCHAR(64) | 审计人 |
+
+**状态机**:`0 未申请 --apply--> 1 申请中 --saveCredentials(验证通过)--> 2 已开通 <--reset--> 1 申请中`;`2 已开通` 下 `toggleEnable` 切换 is_enabled。门店无此行即视为未申请。
+
+---
+
+### 2. PosOrderOmgPayment(OMG 支付流水)— `pos_order_omg_payment`
+
+每笔 OMG 交易一条。承载幂等(按 trade_no)与对账明细。**不复用** `pos_order_payment`。
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | BIGINT PK AUTO | 主键 |
+| dd_id | VARCHAR(64) | 系统订单号(关联 pos_order.dd_id),索引 |
+| merchant_trade_no | VARCHAR(20) | 商店交易编号 MerchantTradeNo(发起生成),索引 |
+| trade_no | VARCHAR(20) | OMG 交易编号 TradeNo(回调获得),唯一索引(幂等键) |
+| store_id | BIGINT | 门店ID |
+| merchant_id | VARCHAR(10) | OMG 商店代号(发起时门店凭证) |
+| choose_payment | VARCHAR(20) | 发起方式:ALL |
+| pay_type | VARCHAR(20) | 回覆 PaymentType: Credit_CreditCard/BarcodeATM_CHINATRUST/ATM_*/CVS_*/AFTEE_AFTEE |
+| amount | INT | 交易金额(整数元 = 订单 amount) |
+| rtn_code | INT | 回调 RtnCode(1=成功) |
+| rtn_msg | VARCHAR(200) | 回调 RtnMsg |
+| pay_status | INT | 0 未支付/1 已支付/2 失败/3 已退款 |
+| auth_code | VARCHAR(20) | 授权码(信用卡回调) |
+| pay_time | DATETIME | 支付完成时间(回调 PaymentDate) |
+| trade_date | DATETIME | 订单创建时间(回调 TradeDate) |
+| callback_raw | TEXT | 回调原始报文(调试/对账) |
+| create_time / update_time | DATETIME | 审计时间 |
+
+**幂等规则**:回调按 `trade_no` 查重——已存在且 `pay_status=1` 则跳过更新(仍回 `1|OK`);不存在则新增/更新。
+**唯一性**:`trade_no` 唯一索引(MySQL 允许多个 NULL,发起时尚无 trade_no);同一订单多次发起产生多行(不同 merchant_trade_no),但同一 trade_no 只对应一行。
+
+---
+
+### 3. PosOrderOmgRefund(OMG 退款记录)— `pos_order_omg_refund`
+
+每次 DoAction 调用一条,与流水解耦,支持多次动作与对账。
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | BIGINT PK AUTO | 主键 |
+| payment_id | BIGINT | 关联 pos_order_omg_payment.id,索引 |
+| dd_id | VARCHAR(64) | 系统订单号,索引 |
+| trade_no | VARCHAR(20) | OMG 交易编号 TradeNo |
+| action | CHAR(1) | DoAction 动作: C/R/E/N |
+| amount | INT | 退款/操作金额 |
+| rtn_code | INT | 响应 RtnCode(1=成功) |
+| rtn_msg | VARCHAR(200) | 响应 RtnMsg |
+| callback_raw | TEXT | 响应原始报文 |
+| create_time | DATETIME | 操作时间 |
+
+---
+
+## 现有实体扩展
+
+### 4. PosOrder(订单)— `pos_order`(不改表结构)
+
+| 字段 | 现状 | 本期用法 |
+|------|------|----------|
+| dd_id | 订单号 | 派生 MerchantTradeNo = "OMG" + dd_id |
+| amount | 订单合计金额(Integer 元) | OMG 支付金额(含运费;与发票 amount−freight 不同) |
+| pay_type | 支付方式字符串 | 新增取值 `"7"` = OMG 在线支付(发起时写入) |
+| pay_status | 0 未支付/1 已支付/2 已退款 | 回调成功置 1(沿用);退款置 2 |
+| pay_url | 支付地址 | 存 OMG gatewayUrl(兜底/记录) |
+| md_id | 门店ID | 决定用哪个门店的 OMG 凭证发起交易 |
+
+**不改表结构**,无 DDL。payType 取值扩展在代码层(常量)体现。
+
+---
+
+## DDL(写入 updatesql/sql.md,不直接执行)
+
+```sql
+-- 2026-07-29 OMG(歐買尬)支付接入 - 门店OMG凭证表
+CREATE TABLE pos_store_omg (
+  id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
+  store_id BIGINT NOT NULL COMMENT '关联 pos_store.id(门店)',
+  omg_status INT NOT NULL DEFAULT 0 COMMENT '申请状态:0未申请/1申请中/2已开通',
+  is_enabled INT NOT NULL DEFAULT 1 COMMENT '启用开关:0停用/1启用',
+  merchant_id VARCHAR(10) DEFAULT NULL COMMENT 'OMG商店代号 MerchantID',
+  hash_key VARCHAR(64) DEFAULT NULL COMMENT 'OMG HashKey',
+  hash_iv VARCHAR(64) DEFAULT NULL COMMENT 'OMG HashIV',
+  enabled_payments VARCHAR(50) DEFAULT 'ALL' COMMENT '启用支付方式(本期ALL)',
+  apply_time DATETIME DEFAULT NULL COMMENT '申请时间',
+  approved_time DATETIME DEFAULT NULL COMMENT '开通时间',
+  last_verify_result VARCHAR(240) DEFAULT NULL COMMENT '最近验证结果',
+  remark VARCHAR(255) DEFAULT NULL COMMENT '备注',
+  create_time DATETIME DEFAULT NULL COMMENT '创建时间',
+  update_time DATETIME DEFAULT NULL COMMENT '更新时间',
+  create_by VARCHAR(64) DEFAULT NULL COMMENT '创建人',
+  update_by VARCHAR(64) DEFAULT NULL COMMENT '更新人',
+  PRIMARY KEY (id),
+  UNIQUE KEY uk_store_id (store_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='门店OMG(歐買尬)支付凭证';
+
+-- 2026-07-29 OMG支付接入 - OMG支付交易流水表
+CREATE TABLE pos_order_omg_payment (
+  id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
+  dd_id VARCHAR(64) NOT NULL COMMENT '系统订单号(关联 pos_order.dd_id)',
+  merchant_trade_no VARCHAR(20) NOT NULL COMMENT '商店交易编号 MerchantTradeNo',
+  trade_no VARCHAR(20) DEFAULT NULL COMMENT 'OMG交易编号 TradeNo(幂等键)',
+  store_id BIGINT DEFAULT NULL COMMENT '门店ID',
+  merchant_id VARCHAR(10) DEFAULT NULL COMMENT 'OMG商店代号',
+  choose_payment VARCHAR(20) DEFAULT 'ALL' COMMENT '发起方式',
+  pay_type VARCHAR(20) DEFAULT NULL COMMENT '回覆PaymentType:Credit_CreditCard/ATM_*/CVS_*/AFTEE_AFTEE/BarcodeATM_CHINATRUST',
+  amount INT DEFAULT NULL COMMENT '交易金额(整数元)',
+  rtn_code INT DEFAULT NULL COMMENT '回调RtnCode(1=成功)',
+  rtn_msg VARCHAR(200) DEFAULT NULL COMMENT '回调RtnMsg',
+  pay_status INT NOT NULL DEFAULT 0 COMMENT '0未支付/1已支付/2失败/3已退款',
+  auth_code VARCHAR(20) DEFAULT NULL COMMENT '授权码',
+  pay_time DATETIME DEFAULT NULL COMMENT '支付完成时间',
+  trade_date DATETIME DEFAULT NULL COMMENT '订单创建时间',
+  callback_raw TEXT COMMENT '回调原始报文',
+  create_time DATETIME DEFAULT NULL COMMENT '创建时间',
+  update_time DATETIME DEFAULT NULL COMMENT '更新时间',
+  PRIMARY KEY (id),
+  UNIQUE KEY uk_trade_no (trade_no),
+  KEY idx_dd_id (dd_id),
+  KEY idx_merchant_trade_no (merchant_trade_no)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='OMG支付交易流水';
+
+-- 2026-07-29 OMG支付接入 - OMG退款记录表
+CREATE TABLE pos_order_omg_refund (
+  id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
+  payment_id BIGINT DEFAULT NULL COMMENT '关联 pos_order_omg_payment.id',
+  dd_id VARCHAR(64) DEFAULT NULL COMMENT '系统订单号',
+  trade_no VARCHAR(20) DEFAULT NULL COMMENT 'OMG交易编号 TradeNo',
+  action CHAR(1) DEFAULT NULL COMMENT 'DoAction动作:C/R/E/N',
+  amount INT DEFAULT NULL COMMENT '操作金额',
+  rtn_code INT DEFAULT NULL COMMENT '响应RtnCode(1=成功)',
+  rtn_msg VARCHAR(200) DEFAULT NULL COMMENT '响应RtnMsg',
+  callback_raw TEXT COMMENT '响应原始报文',
+  create_time DATETIME DEFAULT NULL COMMENT '操作时间',
+  PRIMARY KEY (id),
+  KEY idx_payment_id (payment_id),
+  KEY idx_dd_id (dd_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='OMG退款记录';
+```
+
+注:`uk_trade_no` 唯一索引在 MySQL 下允许多个 NULL(发起时 trade_no 为空),回调写入时保证唯一。
+
+---
+
+## 实体关系
+
+```
+pos_store 1 ──── 1 pos_store_omg            (门店 OMG 凭证)
+pos_store 1 ──── ∞ pos_order                 (门店订单)
+pos_order 1 ──── ∞ pos_order_omg_payment     (一订单多次发起,每发起一行)
+                                   ↑
+                        trade_no 唯一(回调幂等)
+pos_order_omg_payment 1 ──── ∞ pos_order_omg_refund  (一次支付多次退款动作)
+```

+ 107 - 0
specs/016-omg-payment/plan.md

@@ -0,0 +1,107 @@
+# Implementation Plan: OMG(歐買尬/FunPoint)AIO 支付接入
+
+**Branch**: `(待创建,暂未建分支/未提交)` | **Date**: 2026-07-29 | **Spec**: [spec.md](spec.md)
+
+**Input**: Feature specification from `/specs/016-omg-payment/spec.md`
+
+## Summary
+
+接入台湾 OMG(歐買尬,金流引擎 FunPoint)AIO 幕前支付作为餐饮订单的在线支付,替代蓝新 NewebPay(不再启用)。OMG 用 `CheckMacValue`(SHA256) 单字段签名(无 AES、无解密),支持全部支付方式(信用卡/Apple Pay/ATM/超商/BarcodeATM/AFTEE,`ChoosePayment=ALL`),门店级凭证,本期含信用卡退款。**OMG 全部代码独立新建,不复用任何蓝新金流代码/表**;仅复用平台共享基础设施(订单状态机、推送、订单日志)。
+
+技术决策与备选见 [research.md](research.md),表结构见 [data-model.md](data-model.md),接口契约见 [contracts/api.md](contracts/api.md),验证场景见 [quickstart.md](quickstart.md)。
+
+## Technical Context
+
+**Language/Version**: Java 21(Spring Boot 3 / RuoYi-Vue-Plus,MyBatis-Plus,Lombok)
+
+**Primary Dependencies**: Spring Boot、MyBatis-Plus(@TableName/@TableId,XML mapper)、Apache HttpClient 4(幕后 POST QueryTradeInfo/DoAction)、fastjson2、`@Anonymous`/`PermitAllUrlProperties`(回调白名单)
+
+**Storage**: MySQL(新增 `pos_store_omg` / `pos_order_omg_payment` / `pos_order_omg_refund` 三表,DDL 写 `updatesql/sql.md` 不直接执行;`pos_order` 不改结构,仅扩展 `pay_type="7"`)
+
+**Testing**: 工具类 `main` 自测(CheckMacValue 用官方示例值复算)+ 测试环境端到端(测试卡,见 quickstart.md);项目无统一自动化测试框架约定
+
+**Target Platform**: Linux server(后端服务),配合 uni-app 用户端 Form Post 跳转 OMG 收银台
+
+**Project Type**: web-service(后端)
+
+**Performance Goals**: 发起支付接口 < 1s;回调核销幂等无重复发货
+
+**Constraints**: CheckMacValue 必须与 OMG 完全一致(含 .NET 风格 URL 编码与小写);回调须回纯串 `1|OK`;门店级凭证;`InvoiceMark=N`(发票走 ezPay 解耦);外部 HTTP 工具类须放 `ruoyi-admin`(模块依赖 admin→system 不可反向)
+
+**Scale/Scope**: 全平台餐饮订单在线支付(旅游·机票 015 后续衔接,不在本期)
+
+## Constitution Check
+
+*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
+
+`.specify/memory/constitution.md` 为未填写模板(占位符),无实际治理原则可校验。改为遵循项目 `CLAUDE.md` 与记忆中的硬约束:
+
+| 约束 | 本期遵守 |
+|------|----------|
+| 模块依赖 admin→system 不可反向 | ✅ OMG 外部 HTTP/签名工具类放 `ruoyi-admin/.../app/utils/omg/`,联网验证在 Controller 层;实体/Service/Mapper 放 `ruoyi-system` |
+| 外部 HTTP 工具类(httpclient4+fastjson2)放 admin | ✅ `OmgPay`/`OmgCheckMacValue` 在 admin |
+| 后端文件 CRLF | ✅ 新建 Java/md 文件保持 CRLF |
+| DB 变更写 `updatesql/sql.md` 不直接执行 | ✅ 三表 DDL 写入,不执行 |
+| 前端新功能必须 i18n | 本期后端为主;若涉及前端支付跳转/结果页文字,按规范加 4 语言 |
+| 范围控制 / 简单优先 | ✅ 不复用蓝新、不抽公共支付套件(仅一个使用点) |
+| **OMG 不复用蓝新代码**(用户新增) | ✅ 零 `newebpay` import,独立凭证/流水/退款表 |
+
+**结论**:无违规,Phase 0 可继续。Phase 1 设计后再次确认无蓝新耦合。
+
+## Project Structure
+
+### Documentation (this feature)
+
+```text
+specs/016-omg-payment/
+├── plan.md              # 本文件
+├── research.md          # Phase 0:技术决策
+├── data-model.md        # Phase 1:表结构 + DDL
+├── quickstart.md        # Phase 1:端到端验证
+├── contracts/
+│   └── api.md           # Phase 1:OMG 外部接口 + 平台内部接口
+└── tasks.md             # Phase 2 (/speckit-tasks 生成,本阶段不创建)
+```
+
+### Source Code (repository root)
+
+```text
+ruoyi-admin/src/main/java/com/ruoyi/app/
+├── utils/omg/                        # OMG 专属,零 newebpay 依赖
+│   ├── OmgCheckMacValue.java         # CheckMacValue(SHA256) 生成/校验 + main 自测
+│   ├── OmgPayConfig.java             # MerchantID / HashKey / HashIV
+│   └── OmgPay.java                   # HTTP 客户端:createAioForm / queryTrade / doAction
+├── pay/
+│   └── OmgPayController.java         # /pay/omg/{create,notify,return,paymentInfo}(US1/US2/US3)
+└── mendian/
+    └── PosStoreOmgController.java    # /system/storeOmg/* 凭证开通管理(US5)
+
+ruoyi-system/src/main/java/com/ruoyi/system/
+├── domain/
+│   ├── PosStoreOmg.java              # 门店 OMG 凭证(pos_store_omg)
+│   ├── PosOrderOmgPayment.java       # OMG 支付流水(pos_order_omg_payment)
+│   ├── PosOrderOmgRefund.java        # OMG 退款记录(pos_order_omg_refund)
+│   ├── vo/PosStoreOmgVo.java
+│   └── dto/StoreOmgCredentialDto.java
+├── mapper/
+│   ├── PosStoreOmgMapper.java (+xml)
+│   ├── PosOrderOmgPaymentMapper.java (+xml)
+│   └── PosOrderOmgRefundMapper.java (+xml)
+└── service/
+    ├── IPosStoreOmgService.java + impl
+    ├── IPosOrderOmgPaymentService.java + impl
+    └── IPosOrderOmgRefundService.java + impl
+
+updatesql/sql.md                       # 追加三表 DDL(2026-07-29)
+application.yml (+application-dev.yml) # 新增 omg.* 配置段
+```
+
+**Structure Decision**:沿用项目既有分层(utils 在 `ruoyi-admin/.../app/utils/omg/`,domain/mapper/service 在 `ruoyi-system`,Controller 在 `ruoyi-admin/.../app/{pay,mendian}`),与 ezPay/newebpay 同构但**独立包/独立类/独立表**。退款入口(US4)并入 `OmgPayController` 或订单取消链路(`PosOrderShOprate`/`UserOrderController` 取消时触发),tasks 阶段定具体落点。
+
+## Complexity Tracking
+
+> Constitution Check 无违规,无需填表。
+
+| Violation | Why Needed | Simpler Alternative Rejected Because |
+|-----------|------------|-------------------------------------|
+| — | — | — |

+ 72 - 0
specs/016-omg-payment/quickstart.md

@@ -0,0 +1,72 @@
+# Quickstart: OMG(歐買尬/FunPoint)AIO 支付接入
+
+**Feature**: specs/016-omg-payment/spec.md
+**Date**: 2026-07-29
+
+端到端验证 OMG 支付可用性的可运行场景。实现细节见 `tasks.md`(待生成),接口字段见 `contracts/api.md`,表结构见 `data-model.md`。
+
+---
+
+## 前置准备
+
+1. **DB**:执行 `updatesql/sql.md` 中 2026-07-29 的三张表 DDL(`pos_store_omg` / `pos_order_omg_payment` / `pos_order_omg_refund`)。
+2. **配置**(`application.yml`)填入测试值:
+   - `omg.base-url=https://payment-stage.funpoint.com.tw`
+   - `omg.return-url` / `omg.payment-info-url` 指向**公网可达**的本服务地址(本地用 ngrok/frp 穿透到 8082)。
+3. **凭证**:在盘合测试后台申请测试商店号,拿到 `MerchantID` / `HashKey` / `HashIV`。
+4. **签名自测**:运行 `OmgCheckMacValue.main`,应 PASS(用官方示例值复算得 `AA5842FD...CAF93266`)。
+
+---
+
+## 场景 A:门店录入并验证 OMG 凭证(US5)
+
+1. 平台后台对一个门店「发起申请」→「录入凭证」(`MerchantID/HashKey/HashIV`)。
+2. 系统调 OMG `QueryTradeInfo/V5` 查虚构单号探测金钥。
+3. **预期**:金钥正确 → 提示「凭证验证通过,已开通」,`pos_store_omg.omg_status=2, is_enabled=1`;金钥错误 → 拒绝、状态不变、`last_verify_result` 记失败原因。
+
+---
+
+## 场景 B:消费者在线支付(信用卡,US1 + US2)
+
+1. 消费者下一笔餐饮订单(`pos_order.pay_status=0`)。
+2. 调 `POST /pay/omg/create?orderid=...` → 返回 form 字段(`gatewayUrl` + `MerchantID/MerchantTradeNo/.../CheckMacValue`)。
+3. 前端 Form Post 到 OMG 收银台 → 用文档附录**测试卡**完成付款。
+4. OMG 服务端 POST 回 `/pay/omg/notify`。
+5. **预期**:
+   - `/pay/omg/notify` 验签 CheckMacValue 通过、`RtnCode=1`、`SimulatePaid=0` → 回 `1|OK`;
+   - `pos_order_omg_payment.pay_status=1`、`trade_no` 写入、`pay_type=Credit`;
+   - `pos_order.pay_status=1`(state 不变)、订单日志记录、用户/商家/骑手收到推送(含 `sendAcceptRiderPush`)。
+
+---
+
+## 场景 C:回调健壮性(US2 边界)
+
+1. **重复回调**:对同一 `trade_no` 再发一次 notify → 不重复更新订单、不重复推送,仍回 `1|OK`。
+2. **伪造回调**:篡改金额或 CheckMacValue → 验签失败、拒绝、订单状态不变、记日志。
+3. **金额不符**:`TradeAmt` ≠ 订单 amount → 拒绝核销、记日志。
+4. **测试模拟**:`SimulatePaid=1` → 不核销发货、记日志。
+
+---
+
+## 场景 D:非即时支付 ATM/超商(US3)
+
+1. `ChoosePayment=ALL` 时消费者在 OMG 选 ATM 或超商。
+2. OMG 回调 `/pay/omg/paymentInfo` 带虚帐/缴费码 → 平台展示(前端页)与期限。
+3. 消费者期限内付款 → OMG 回调 `/pay/omg/notify`(`RtnCode=1`)→ 同场景 B 核销。
+4. **预期**:`pay_type=ATM/CVS`,超期未付订单可重试或失效。
+
+---
+
+## 场景 E:订单取消退款(US4,仅信用卡)
+
+1. 对一笔已支付信用卡订单发起取消。
+2. 系统调 `/CreditDetail/DoAction`(Action 按状态,默认 `R` 退刷)→ 写 `pos_order_omg_refund`。
+3. **预期**:成功 → `pos_order_omg_payment.pay_status=3(已退款)`、`pos_order.pay_status=2`、`pos_order_omg_refund.rtn_code=1`;失败 → 记 `rtn_msg` 可重试、不误标已退款。
+4. ⚠️ **stage 环境 `DoAction` 不可用**,退款须在正式环境做小额验证或 mock 测试。
+
+---
+
+## 完成判据
+
+- 场景 A/B 全绿;C 的伪造/重复/金额不符/模拟均被正确拦截;D 的 ATM/超商能拿到付款信息并核销;E 的信用卡退款状态与资金同步。
+- 全程 OMG 代码无任何 `newebpay` 依赖(grep 验证)。

+ 135 - 0
specs/016-omg-payment/research.md

@@ -0,0 +1,135 @@
+# Research: OMG(歐買尬/FunPoint)AIO 支付接入
+
+**Feature**: specs/016-omg-payment/spec.md
+**Date**: 2026-07-29
+
+本文记录 OMG 支付接入的关键技术决策、理由与备选。所有外部接口字段已对照 OMG 官方文档(`developers.omg.com.tw/payment/aio/`)确认。
+
+---
+
+## 0. 总体约束:OMG 全部独立实现,不复用蓝新(NewebPay)代码
+
+**Decision**:OMG 支付的所有代码(签名工具、HTTP 客户端、实体、表、Service、Controller)均为 OMG **独立新建**,**不得 import 或依赖任何蓝新金流代码/表**,包括:`com.ruoyi.app.utils.newebpay.*`、`NewebPayEncryptUtil`、`NewebPay`、`NewebpayPayController`、`PosStoreNewebpay*`、以及蓝新建的流水表 `pos_order_payment`。
+
+**Rationale**:用户明确要求。蓝新(011)代码就绪但从未上线、本期 D2 被 OMG 替代且不再启用;让 OMG 与蓝新在代码与数据上彻底解耦,避免被废弃代码牵连,便于将来直接删除蓝新代码。OMG 与蓝新结构相似仅因同属「幕前 AIO 支付」问题域(API 形态决定),属独立重写而非复用。
+
+**允许使用的是「平台共享基础设施」(非蓝新代码)**:`PosOrder`/`IPosOrderService`(订单状态机)、`PayPush` + `PushEventService`(推送)、`OrderLogHelper`(订单日志)、`sendAcceptRiderPush`(可接单骑手推送)、`IpnLog`(通用 IPN 日志)、`InfoUser`、`@Anonymous`/`PermitAllUrlProperties`。这些是平台公共能力,不属于蓝新。
+
+---
+
+## D1. 签名工具:新建 OmgCheckMacValue(CheckMacValue / SHA256),独立于蓝新
+
+**Decision**:新建 `ruoyi-admin/.../app/utils/omg/OmgCheckMacValue.java`(静态,CheckMacValue 生成 + 校验)+ `OmgPay.java`(HTTP 客户端)+ `OmgPayConfig.java`(MerchantID/HashKey/HashIV),独立包 `utils/omg`,不依赖 `newebpay`。
+
+**算法**(EncryptType=1,SHA256):
+1. 去掉 `CheckMacValue` 本身;其余参数按 key 字母序升序排列。
+2. 拼 `k1=v1&k2=v2...`。
+3. 包夹:`HashKey={key}&{query}&HashIV={iv}`。
+4. **.NET 风格 URL 编码**(见 reference 05 表):`- _ . ! * ( )` 不编码、`~`→`%7e`、空格→`+`、其余特殊符 `%XX`、中文 UTF-8 `%XX`。**Java 实现**:`URLEncoder.encode(s, UTF_8)`(空格已→`+`)后,照 PHP 范例替换 `%2d→- %5f→_ %2e→. %21→! %2a→* %28→( %29→)`(Java 本就不编 `-_.`,`!*()` 需替换回),再转小写。
+5. 整串转**小写**。
+6. **SHA256** → hex 转**大写** = CheckMacValue。
+7. 校验:对回调参数同算法重算,与回传 CheckMacValue 比对,不一致即拒绝。
+
+**与蓝新的本质区别**:蓝新是 AES-256-CBC 加密 TradeInfo + SHA256 TradeSha,回调需 AES 解密;**OMG 无 AES、无解密**,请求与回调都是「明文参数 + 单个 CheckMacValue」。实现更简单,故绝不复用 `NewebPayEncryptUtil`。
+
+**自测**:用官方示例 HashKey=`5294y06JbISpM5x9`、HashIV=`v77hoKGq4kWxNNIS` 的参数复算,应得 `AA5842FDA7E55ACEB7118D6353E9822CA6D6FF09A0D1FC129A879DD5CAF93266`(`OmgCheckMacValue.main` 自测)。
+
+---
+
+## D2. 支付流水表:新建 pos_order_omg_payment(不复用蓝新的 pos_order_payment)
+
+**Decision**:新建 `pos_order_omg_payment`(每笔 OMG 交易一条,按 `trade_no` = OMG `TradeNo` 幂等),**不复用** 011 的 `pos_order_payment`(复用即复用蓝新代码/数据,违背约束 0)。
+
+**字段**:`dd_id`、`merchant_trade_no`(商店交易编号 MerchantTradeNo,发起生成)、`trade_no`(OMG 交易编号 TradeNo,回调获得,幂等键,唯一索引)、`store_id`、`merchant_id`、`choose_payment`(发起 ALL)、`pay_type`(回调方式 Credit/ATM/CVS/ApplePay/BarcodeATM/AFTEE)、`amount`、`rtn_code`、`rtn_msg`、`pay_status`(0 未支付/1 已支付/2 失败/3 已退款)、`auth_code`、`pay_time`(PaymentDate)、`trade_date`、`callback_raw`、审计时间。唯一索引 `trade_no`。
+
+**退款**:新建 `pos_order_omg_refund`(每次 DoAction 一行:`payment_id`/`trade_no`/`action`/`amount`/`rtn_code`/`rtn_msg`/`callback_raw`/`time`),与流水解耦,支持多次动作与对账。
+
+**幂等**:回调按 `trade_no` 查重;已 `pay_status=1` 则跳过更新(仍回 `1|OK`)。
+
+---
+
+## D3. 门店凭证表:新建 pos_store_omg(独立于蓝新凭证表)
+
+**Decision**:新建 `pos_store_omg`(与 `pos_store` 1:1),状态机沿用 009/011 的「0 未申请 / 1 申请中 / 2 已开通 + is_enabled」模式,但**独立表独立字段**,不复用 `pos_store_newebpay`。字段:`store_id`(UK)、`omg_status`、`is_enabled`、`merchant_id`、`hash_key`、`hash_iv`、`enabled_payments`(本期固定 ALL,字段保留以备收敛)、`apply_time`、`approved_time`、`last_verify_result`、`remark` + 审计字段。
+
+---
+
+## D4. 凭证联网验证:用 OMG QueryTradeInfo/V5 探测
+
+**Decision**:录入凭证后调 OMG `Cashier/QueryTradeInfo/V5`(stage 可用)查一个虚构 `MerchantTradeNo` 探测金钥。金钥/商店代号错误 → 响应状态非正常且 Message 提示金钥/商店;金钥正确 → 返回「无此交易」类结果(TradeStatus 异常但非金钥错误)。验证逻辑放 Controller 层(同 011/009 思路,避免 ruoyi-system 反向依赖 ruoyi-admin 工具类),Service 仅持久化结果。
+
+---
+
+## D5. 回调链路:OMG Controller 独立实现,复用平台共享推送基础设施
+
+**Decision**:新建 `OmgPayController`(`/pay/omg/*`),`/pay/omg/notify` 加 `@Anonymous`。流程:collectForm → IpnLog → 按 `MerchantID` 查 `pos_store_omg` 凭证 → **验签 CheckMacValue(无需解密)** → 幂等(`trade_no`)→ 金额校验(`TradeAmt`)→ `RtnCode==1 且 SimulatePaid!=1` → markSuccess → 更新订单(`state=0`,`payStatus=1`)+ 订单日志 + `PayPush`/`PushEventService` 推送用户/商家/骑手 + **`sendAcceptRiderPush` 推送可接单骑手**。
+
+**回应差异**:OMG 要求回应**纯字符串 `1|OK`**(蓝新是 JSON `{Status:SUCCESS}`,不同),未收到会在 5–15 分钟后重试,当天最多 4 次。
+
+**sendAcceptRiderPush 迁移**:CLAUDE.md 记「`sendAcceptRiderPush` 迁移推迟到接入新支付时再做」——OMG 即新支付,本期在 `OmgPayController` 内完成该推送接线(复用 `PosOrderController` 货到付款路径同款调用);蓝新侧的空 TODO 不再回头补。
+
+---
+
+## D6. MerchantTradeNo 生成规则
+
+**Decision**:`MerchantTradeNo = "OMG" + ddId`(ddId 清洗为英数,≤20 字元),前缀区别蓝新 `"NB"`;重新发起追加时间戳后缀,旧记录作废。OMG 要求英数大小写混合、≤20。
+
+---
+
+## D7. 退款(本期 D3=做):DoAction,仅信用卡;ATM/超商人工
+
+**Decision**:订单取消时,**信用卡**订单调 `/CreditDetail/DoAction`,Action 按状态:`已關帳→R(退刷)`、`已授權→N(放棄)`、`要關帳→E(取消)再 N 或 R`,**全额退款**(分期必须全额);写入 `pos_order_omg_refund`。`ATM/超商/BarcodeATM` 无退款 API → 记录待人工在盘合后台处理。
+
+**Action 选择依赖状态**:需先 `CreditDetail/QueryTrade/V2` 查信用卡状态(需下单时 `NeedExtraPaidInfo=Y` 拿 `gwsr` + 盘合后台 `CreditCheckCode`)。MVP 可:已支付订单统一先尝试 `R(退刷)`,失败再按状态分支。**测试环境 DoAction 不可用** → 退款联调需正式小额或 mock。
+
+---
+
+## D8. 环境配置:application.yml 新增 omg 段
+
+```yaml
+omg:
+  base-url: https://payment-stage.funpoint.com.tw      # 测试 stage / 正式 payment.funpoint.com.tw
+  return-url: https://<公网域名>/pay/omg/notify         # 服务端回调(须回 1|OK)
+  order-result-url: https://<前端>/pay-result           # 客户端跳转(可选,即时方式)
+  payment-info-url: https://<公网域名>/pay/omg/paymentInfo  # ATM/超商虚帐/缴费码回调
+  client-redirect-url: https://<前端>/payment-info       # ATM/超商客户端展示
+```
+
+`InvoiceMark=N` 固定(发票走 ezPay,与支付解耦);`EncryptType=1` 固定。
+
+---
+
+## D9. payType 取值
+
+**Decision**:`PosOrder.payType` 新增 `"7"` = OMG 在线支付(蓝新 `"6"`,OMG `"7"`,独立值便于辨识与过渡);具体方式由 `pos_order_omg_payment.pay_type` 在回调后承载。不改 `pos_order` 表结构(代码常量体现)。
+
+---
+
+## D10. 测试环境
+
+stage `payment-stage.funpoint.com.tw` + 测试商店凭证/测试卡(文档附录)。NotifyURL 经内网穿透(ngrok/frp)暴露。链路:录入测试凭证 → 下单 → 发起 OMG → 测试卡付款 → 回调 → 订单已支付 + 推送。退款:stage `DoAction` 不可用,需正式小额或 mock。
+
+---
+
+## D11. 数据库变更管理
+
+`pos_store_omg` / `pos_order_omg_payment` / `pos_order_omg_refund` 建表 DDL 写入 `updatesql/sql.md`(标注日期与用途),**不直接执行**,由开发者统一手动执行(遵循项目规范)。`pos_order` 不改结构。
+
+---
+
+## D12. 残留待确认(不阻塞 plan 推进,留待 tasks/实现)
+
+- `NeedExtraPaidInfo` 是否本期开启(影响退款状态查询与 `gwsr`)——建议开启。
+- `CreditCheckCode` 来源(盘合后台)与是否配置化(影响信用卡明细查询/退款 Action 判定)。
+- `OrderResultURL`/`PaymentInfoURL` 对应前端页是否本期做(US3 ATM/超商展示依赖前端页)。
+- 自动关帳(每日自動關帳)是否开启(影响 DoAction 调用时段,避开 20:15–20:30)。
+
+---
+
+## D13. 代码表与枚举(reference 02–05,2026-07-29 补读)
+
+- **ChoosePayment**(请求):本期 `ALL`;细分 Credit(信用卡/銀聯/Apple Pay)/ATM(FIRST/CHINATRUST/UBOT/KGI)/CVS(CVS/FAMILY/IBON/HILIFE)/BarcodeATM(CHINATRUST)/AFTEE。
+- **Apple Pay 归在 Credit 下**(非独立 ChoosePayment);回覆 PaymentType 为 `Credit_CreditCard`,**仅凭 PaymentType 无法区分信用卡与 Apple Pay**(本期不区分)。
+- **回覆 PaymentType**(回调,落 `pos_order_omg_payment.pay_type`):`Credit_CreditCard`、`BarcodeATM_CHINATRUST`(即时);`ATM_FIRST/CHINATRUST/UBOT/KGI`、`CVS_CVS/FAMILY/IBON/HILIFE`、`AFTEE_AFTEE`(延期,走 PaymentInfoURL 取号 → US3)。
+- **RtnCode**:`1`=成功,其余失败;完整表为**图片**且持续新增,须查「歐買尬廠商後台→系統開發管理→交易狀態代碼查詢」。实现:`==1` 成功核销/退款成功,非 1 失败并记 `rtn_code`+`rtn_msg`,**不硬编码错误码分支**。
+- **URLEncode(CheckMacValue)**:照 reference 05 的 .NET 表,Java 精确实现见 contracts/api.md §C4 与上文 D1 step4。

+ 184 - 0
specs/016-omg-payment/spec.md

@@ -0,0 +1,184 @@
+# Feature Specification: OMG 支付接入(订单在线支付)
+
+**Feature Branch**: `(待创建,暂未建分支/未提交)`
+
+**Created**: 2026-07-29
+
+**Status**: Draft —— 关键决策已定(D1–D6,见「已定决策」)
+
+**Input**: 接入台湾 OMG(歐買尬 / 金流引擎 FunPoint)AIO 幕前支付(`https://developers.omg.com.tw/payment/aio/api/01_order.html`)用于订单在线支付。替代已就绪未启用的蓝新 NewebPay(011,不再启用);OMG 独立实现、不复用蓝新代码,仅复用平台共享订单/推送链路。
+
+---
+
+## 集成背景(已确认,来自 OMG AIO 文档)
+
+- **网关端点**:
+  - 测试:`https://payment-stage.funpoint.com.tw/Cashier/AioCheckOut/V5`
+  - 生产:`https://payment.funpoint.com.tw/Cashier/AioCheckOut/V5`
+  - `POST`,`application/x-www-form-urlencoded`
+- **签名模型(关键差异)**:OMG 用 **`CheckMacValue`(SHA256,`EncryptType=1`)** 单字段签名;这与蓝新 NewebPay 的 `TradeInfo`(AES) + `TradeSha`(SHA256) 双层模型**不同**。结论:**需独立的 OMG 签名工具,不能复用 `NewebPayEncryptUtil`**。具体算法(参数排序、URI 编码、HashKey/HashIV 包夹、SHA256 大写)在文档附錄 `07_appendices` / 范例 `06_check-value-sample`,留待 plan/research 落地。
+- **支持的支付方式**(`ChoosePayment`):`Credit`(信用卡)、`ApplePay`、`ATM`、`CVS`(超商代码)、`BarcodeATM`、`AFTEE`、`ALL`。
+  - ⚠️ 文档**未把「LINE Pay」列为独立 ChoosePayment** —— 用户提及 LINE Pay,需确认其接入方式(可能走 Credit 子支付或独立通道),见 D4。
+- **幕前流程**:商户组参 + 计算 `CheckMacValue` → POST 到 `AioCheckOut/V5` → 消费者在 OMG 收银台支付 → OMG **服务端 POST 回 `ReturnURL`**(商户须回 `1|OK`)→ 可选 `OrderResultURL` 客户端跳转(仅即时支付方式支持)。
+- **非即时支付(ATM/超商/BarcodeATM)**:OMG 生成虚帐/缴费码 → 回调 `PaymentInfoURL`(服务端 POST)+ 可选 `ClientRedirectURL`(客户端展示)。
+- **退款**:`05_refund` API;**订单查询**:`04_order_query`;**付款结果通知**:`03_payment_notify`。
+- **发票**:`InvoiceMark` 必须 `N` —— 电子发票仍走本项目 ezPay 链路(010/014),与支付解耦。
+- **约束**:不可用 iframe;iOS 下不可另开新窗口;`MerchantTradeNo` 全平台唯一;`ReturnURL` 与 `OrderResultURL` 不可相同。
+
+## 与本项目现有支付的关系
+
+- 本功能把 **OMG 启用为订单在线支付**。蓝新 NewebPay(011)代码就绪**未启用**;VNPay/ZaloPay 已废弃(见 `project-deprecated-code` 记忆)。
+- **复用现有链路**:订单状态流转入口(`PosOrderShOprate`/`PosOrderQsOprate`/`UserOrderController`/`PosOrderController`)、`PayPush`(iOS uni 云函数 msduser/msdrider/msdstore + `PushEvent`→`push_message`)、以及 `sendAcceptRiderPush`(迁移契机,见 CLAUDE.md「当前在用的订单操作入口」)。
+- **凭证门店级**:每门店独立 OMG 凭证(MerchantID/HashKey/HashIV),存于门店维度配置表 `pos_store_omg`。
+- **不复用蓝新代码(用户硬约束)**:OMG 全部代码(工具类/客户端/实体/Service/Controller)与表(凭证/流水/退款)独立新建、零 `newebpay` 依赖,不复用蓝新的 `pos_store_newebpay` / `pos_order_payment`;仅复用平台共享基础设施(订单状态机、推送、订单日志)。
+
+---
+
+## 已定决策(D1–D6,2026-07-29 确认)
+
+- **D1 — 支付方式范围**:支持 OMG 全部支付方式(`ChoosePayment=ALL`:信用卡 Credit / Apple Pay / ATM / 超商 CVS / BarcodeATM / AFTEE)。
+- **D2 — 与 NewebPay(011) 的关系**:**OMG 替代蓝新金流**,作为订单在线支付启用;NewebPay 不再启用。
+- **D3 — 退款/取消**:**本期范围**,订单取消走 OMG 退款 API(`05_refund`)。
+- **D4 — LINE Pay**:**本期不做**(文档未列为独立 ChoosePayment,后续再议)。
+- **D5 — 覆盖订单范围**:**本期仅餐饮订单**;旅游·机票订单(015)后续衔接,不在本期。
+- **D6 — 凭证粒度**:**门店级**(每门店独立 MerchantID / HashKey / HashIV,启用开关,同 011 模式)。
+
+> 说明:OMG 文档可正常获取(与 015 的 iFlight 不同),无硬阻塞。决策已定,可直接进 `/speckit-plan`。
+
+---
+
+## User Scenarios & Testing *(mandatory)*
+
+### User Story 1 - 消费者在线支付订单(Priority: P1)
+
+消费者下单后选择在线支付,选定方式(如信用卡/Apple Pay),跳转 OMG 收银台完成付款;付款成功后订单变为已支付并进入履约。
+
+**Why this priority**:在线支付是订单交易闭环的核心,没有它订单无法在线收款。
+
+**Independent Test**:从一笔待支付订单出发,能跳转 OMG、用测试卡完成支付,订单状态变为已支付。
+
+**Acceptance Scenarios**:
+
+1. **Given** 一笔待支付订单,**When** 消费者发起支付并选择支付方式,**Then** 跳转 OMG 收银台且金额/订单号正确。
+2. **Given** 消费者在 OMG 完成付款,**When** OMG 回调平台,**Then** 订单核销为已支付,消费者看到支付成功。
+3. **Given** 消费者取消或关闭支付页未付款,**When** 超时,**Then** 订单保持未支付并可重试,不误判为已付。
+
+---
+
+### User Story 2 - 支付回调核销与履约触发(Priority: P2)
+
+OMG 服务端回调(`ReturnURL`)到达后,验签 + 幂等更新订单,并触发后续履约(状态流转、商家/骑手推送)。
+
+**Why this priority**:支付成功必须可靠地驱动订单履约,否则「付了钱却没人接单」。
+
+**Independent Test**:模拟 OMG 回调(合法/伪造/重复),合法回调能更新订单并触发推送,伪造被拒、重复不重复发货。
+
+**Acceptance Scenarios**:
+
+1. **Given** OMG 合法回调到达,**When** 平台验签通过,**Then** 订单更新为已支付并回 `1|OK`,履约链路(含 `sendAcceptRiderPush` 推送)被触发。
+2. **Given** 同一订单的重复回调,**When** 再次到达,**Then** 幂等处理,不重复更新/不重复推送。
+3. **Given** 伪造或验签失败的回调,**When** 到达,**Then** 拒绝并记录,订单状态不变。
+
+---
+
+### User Story 3 - 非即时支付(ATM / 超商)(Priority: P3)
+
+消费者选择 ATM 转账或超商缴费时,平台展示 OMG 生成的虚帐/缴费码,消费者在期限内完成后由 OMG 回调核销。
+
+**Why this priority**:扩大支付覆盖面,但非即时到账,可晚于信用卡 MVP。
+
+**Independent Test**:选择 ATM/超商,能拿到虚帐/缴费码并展示;模拟付款完成回调能核销订单。
+
+**Acceptance Scenarios**:
+
+1. **Given** 消费者选 ATM/超商,**When** OMG 生成虚帐/缴费码回调 `PaymentInfoURL`,**Then** 平台展示付款信息与期限。
+2. **Given** 消费者在期限内完成付款,**When** OMG 回调 `ReturnURL`,**Then** 订单核销为已支付并触发履约。
+3. **Given** 超过付款期限未付,**When** 逾期,**Then** 订单按规则失效/可重试。
+
+---
+
+### User Story 4 - 订单取消与退款(Priority: P3)
+
+订单取消(用户取消/商家取消/超时退款)时,通过 OMG 退款 API(`05_refund`)原路退款,并同步状态。
+
+**Why this priority**:售后资金闭环,依赖 D3 是否本期。
+
+**Independent Test**:对一笔已支付订单发起取消,能调用 OMG 退款并同步为已退款。
+
+**Acceptance Scenarios**:
+
+1. **Given** 一笔已支付订单可退款,**When** 发起取消,**Then** 调 OMG 退款 API 并将订单标记为退款中/已退款。
+2. **Given** 退款失败,**When** OMG 返回失败,**Then** 记录原因并可重试,不误标已退款。
+
+---
+
+### User Story 5 - 商家/平台配置 OMG 凭证(Priority: P2)
+
+商家或平台在后台维护 OMG 凭证(MerchantID/HashKey/HashIV),支持测试/生产切换与启用开关。
+
+**Why this priority**:支付前提是凭证可用,属 P2 基础设施。
+
+**Independent Test**:在后台录入门店 OMG 凭证,能用于发起支付与验签。
+
+**Acceptance Scenarios**:
+
+1. **Given** 已授权运营,**When** 录入门店 OMG 凭证并启用,**Then** 该门店订单可发起 OMG 支付。
+2. **Given** 未配置或未启用凭证的门店,**When** 尝试在线支付,**Then** 不展示/不可用在线支付(可降级货到付款)。
+
+---
+
+### Edge Cases
+
+- 回调签名验证失败(伪造/篡改)—— 必须拒绝并记录。
+- 重复回调(网络重试)—— 按 `MerchantTradeNo` 幂等,不重复发货/推送。
+- 消费者支付页关闭/超时未付 —— 订单不误判,可重试或按规则失效。
+- 金额不一致(下单价 vs 支付价,如改了单/用了券)—— 以平台实际应收金额为准。
+- 回调与「用户取消」并发 —— 明确最终状态优先级,避免已付款却被取消。
+- 退款失败 / 部分退款 / 跨日退款。
+- 多门店凭证:A 门店凭证不可用于 B 门店订单。
+- 测试凭证误用到生产、或反之。
+
+## Requirements *(mandatory)*
+
+### Functional Requirements
+
+- **FR-001**:系统 MUST 支持消费者对未支付订单发起 OMG 幕前在线支付,跳转 OMG 收银台(金额、订单号、商品描述正确)。
+- **FR-002**:系统 MUST 支持 OMG 全部支付方式(`ChoosePayment=ALL`:信用卡 / Apple Pay / ATM / 超商 / BarcodeATM / AFTEE);LINE Pay 本期不做(D4)。
+- **FR-003**:系统 MUST 使用 OMG `CheckMacValue`(SHA256,`EncryptType=1`)对请求签名、对回调验签;签名工具**独立于**现有 NewebPay 工具。
+- **FR-004**:系统 MUST 在 OMG `ReturnURL` 回调时验签 + 按 `MerchantTradeNo` 幂等更新订单为已支付,并回 `1|OK`。
+- **FR-005**:系统 MUST 在支付成功后触发现有订单履约链路(状态流转 + iOS 推送 + `sendAcceptRiderPush`)。
+- **FR-006**:系统 MUST 支持非即时支付(ATM/超商)的虚帐/缴费码生成、展示与期限管理(`PaymentInfoURL` 回调)。
+- **FR-007**:系统 MUST 支持订单取消时经 OMG 退款 API(`05_refund`)退款并同步状态(本期范围,D3)。
+- **FR-008**:系统 MUST 支持门店级 OMG 凭证配置(MerchantID/HashKey/HashIV,启用开关,测试/生产切换)。
+- **FR-009**:本期功能覆盖范围**仅餐饮订单**的在线支付;旅游·机票订单(015)后续衔接,不在本期(D5)。
+- **FR-010**:系统 MUST 持久化支付流水(`MerchantTradeNo`、金额、方式、时间、回调原始报文、退款记录),支持对账。
+- **FR-011**:系统 MUST 在 `InvoiceMark=N` 下与 ezPay 电子发票链路解耦,支付不干涉开票。
+- **FR-012**:OMG **替代蓝新 NewebPay(011)** 作为订单在线支付启用,NewebPay 不再启用(D2)。
+- **FR-013**:系统 MUST NOT 复用或依赖任何蓝新 NewebPay 代码/表;OMG 凭证、流水、退款表与工具类/Controller 全部独立新建,仅可使用平台共享基础设施(订单状态机、推送、订单日志)。
+
+### Key Entities *(include if feature involves data)*
+
+- **门店 OMG 凭证(StoreOmgCredential)**:门店关联、MerchantID、HashKey、HashIV、环境(测试/生产)、启用开关。
+- **支付请求(OmgPaymentRequest)**:`MerchantTradeNo`(幂等键)、订单、金额、`ChoosePayment`、ReturnURL/NotifyURL、生成的 `CheckMacValue`。
+- **支付流水(OrderPayment)**:`MerchantTradeNo`、关联订单、金额、支付方式、状态(待付/已付/失败/退款)、OMG 交易号、回调原始报文、时间。
+- **退款记录(OmgRefund)**:关联支付流水、退款金额、退款状态、OMG 回应、时间。
+
+## Success Criteria *(mandatory)*
+
+### Measurable Outcomes
+
+- **SC-001**:消费者从下单到完成在线支付(即时方式)端到端时长满足体验预期(如主流在数分钟内)。
+- **SC-002**:支付回调核销成功率达标;重复回调 **100% 幂等**,不重复发货/推送。
+- **SC-003**:支付成功后订单履约(状态流转 + 推送)**100% 被触发**,无「已付款未接单」。
+- **SC-004**:伪造/验签失败的回调 **100% 被拒绝**,资金安全零失误。
+- **SC-005**:退款(若本期)在目标时长内完成且订单状态与资金同步。
+
+## Assumptions
+
+- 平台已/将向 OMG 申请商店号与 HashKey/HashIV(测试 + 生产)。
+- 复用现有订单状态机、`PayPush` 推送链路、用户体系,不为此功能重建。
+- OMG AIO `CheckMacValue` 算法与 ECPay 同源(业界标准),具体实现留待 plan/research。
+- 电子发票仍走 ezPay(010/014),与 OMG 支付解耦(`InvoiceMark=N`)。
+- 测试先用 `payment-stage.funpoint.com.tw` 测试端点与测试凭证,生产环境与凭证后续切换。
+- 凭证签名等敏感操作在服务端完成,HashKey/HashIV 不下发前端。
+- 在线支付与现有「货到付款」并存,由门店配置决定是否提供在线支付。

+ 217 - 0
specs/016-omg-payment/tasks.md

@@ -0,0 +1,217 @@
+---
+description: "Task list for OMG(歐買尬/FunPoint)AIO 支付接入"
+---
+
+# Tasks: OMG(歐買尬/FunPoint)AIO 支付接入
+
+**Input**: Design documents from `/specs/016-omg-payment/`
+
+**Prerequisites**: plan.md, spec.md, research.md(D1–D13), data-model.md, contracts/api.md(A1–A5/B1–B7/§C), quickstart.md(均已就绪)
+
+**Tests**: 未要求 TDD;验证以 `OmgCheckMacValue.main` 自测(官方示例值)+ quickstart.md 场景 A–E 端到端为准。
+
+**Organization**: 按 user story 组织,每个 story 可独立实现与验证。**MVP = US1 + US2**(支付闭环)。
+
+**⚠️ 硬约束(research §0)**:OMG 全部代码独立新建,**零 `newebpay` 依赖**(不得 import `com.ruoyi.app.utils.newebpay.*`、`NewebPayEncryptUtil`、`NewebPay`、`NewebpayPayController`、`PosStoreNewebpay*`,不复用 `pos_order_payment` 表)。仅复用平台共享基础设施:`PosOrder`/`IPosOrderService`、`PayPush`/`PushEventService`、`OrderLogHelper`、`IpnLog`、`OrderPushBodyDto`、`sendAcceptRiderPush`/`sendHdfkMessage` 链路、`InfoUser`、`@Anonymous`。
+
+## Format: `[ID] [P?] [Story] Description`
+
+- **[P]**: 可并行(不同文件、无依赖)
+- **[Story]**: 归属 user story(US1~US5)
+- 描述含精确文件路径;前端 CRLF 文件用 Python 脚本编辑
+
+---
+
+## Phase 1: Setup(基础设施)
+
+**Purpose**: 数据库 DDL 与配置就位
+
+- [ ] T001 追加三表建表 DDL 到 `updatesql/sql.md`(`pos_store_omg`、`pos_order_omg_payment`、`pos_order_omg_refund`,取自 data-model.md,标注 2026-07-29 OMG 支付接入;不直接执行)
+- [ ] T002 [P] 在 `ruoyi-admin/src/main/resources/application.yml`(及 `application-dev.yml`)新增 `omg` 配置段(见 research D8/contracts B1):`base-url=https://payment-stage.funpoint.com.tw`(正式 `payment.funpoint.com.tw`)、`return-url`、`order-result-url`、`payment-info-url`、`client-redirect-url`;`InvoiceMark=N`、`EncryptType=1` 在代码固定,不入配置
+
+---
+
+## Phase 2: Foundational(阻塞前置,所有 story 依赖)
+
+**Purpose**: 签名工具、HTTP 客户端、三实体、Mapper——所有 story 的公共地基
+
+**⚠️ CRITICAL**: 本阶段完成前(尤其 `OmgCheckMacValue` 自测通过)不得开始任何 user story
+
+- [ ] T003 [P] 创建 `ruoyi-admin/src/main/java/com/ruoyi/app/utils/omg/OmgPayConfig.java`(merchantId/hashKey/hashIv,构造器+getter,仿 `NewebPayConfig` 结构但独立包)
+- [ ] T004 创建 `ruoyi-admin/src/main/java/com/ruoyi/app/utils/omg/OmgCheckMacValue.java`:静态 `generate(Map params, hashKey, hashIv)` 与 `verify(Map params, hashKey, hashIv)`。算法(contracts §C4 / research D1):去 CheckMacValue → key 字母序 → `k=v&...` → 包夹 `HashKey={k}&..&HashIV={iv}` → `URLEncoder.encode(s,UTF_8)` → 替换 `%2d→- %5f→_ %2e→. %21→! %2a→* %28→( %29→)` → 转小写 → SHA256 → hex 大写。含 `main` 自测:用官方示例 HashKey=`5294y06JbISpM5x9`/HashIV=`v77hoKGq4kWxNNIS` 复算应得 `AA5842FDA7E55ACEB7118D6353E9822CA6D6FF09A0D1FC129A879DD5CAF93266`
+- [ ] T005 [P] 创建 `ruoyi-system/src/main/java/com/ruoyi/system/domain/PosStoreOmg.java`(`@TableName("pos_store_omg")`,字段见 data-model.md §1)
+- [ ] T006 [P] 创建 `ruoyi-system/src/main/java/com/ruoyi/system/domain/PosOrderOmgPayment.java`(`@TableName("pos_order_omg_payment")`,字段见 data-model.md §2)
+- [ ] T007 [P] 创建 `ruoyi-system/src/main/java/com/ruoyi/system/domain/PosOrderOmgRefund.java`(`@TableName("pos_order_omg_refund")`,字段见 data-model.md §3)
+- [ ] T008 [P] 创建 `ruoyi-system/src/main/java/com/ruoyi/system/mapper/PosStoreOmgMapper.java` + `ruoyi-system/src/main/resources/mapper/chanting/PosStoreOmgMapper.xml`(基础 CRUD + selectStoreList/selectStoreDetail VO 查询,仿 PosStoreEzpayMapper)
+- [ ] T009 [P] 创建 `ruoyi-system/src/main/java/com/ruoyi/system/mapper/PosOrderOmgPaymentMapper.java` + `.../mapper/chanting/PosOrderOmgPaymentMapper.xml`(insert、selectByTradeNo、selectByMerchantTradeNo、selectLatestByDdId、updateById)
+- [ ] T010 [P] 创建 `ruoyi-system/src/main/java/com/ruoyi/system/mapper/PosOrderOmgRefundMapper.java` + `.../mapper/chanting/PosOrderOmgRefundMapper.xml`(insert、selectByPaymentId、selectByDdId)
+- [ ] T011 [P] 创建 `ruoyi-admin/src/main/java/com/ruoyi/app/utils/omg/OmgPay.java`(`@Component`,HTTP 客户端,httpclient4+fastjson2):端点常量 `URL_AIO_CHECKOUT=/Cashier/AioCheckOut/V5`、`URL_QUERY_TRADE_INFO=/Cashier/QueryTradeInfo/V5`、`URL_DO_ACTION=/CreditDetail/DoAction`;`createAioForm(baseUrl,cfg,params)`→对全部参数生成 CheckMacValue 后返回 form 字段 Map(含 gatewayUrl);`queryTrade(baseUrl,cfg,merchantTradeNo)`→响应 k=v 解析为 Map;`doAction(baseUrl,cfg,merchantTradeNo,tradeNo,action,totalAmount)`→响应 Map;私有 `postForm`
+
+**Checkpoint**:`OmgCheckMacValue.main` 自测通过、三表可建、实体/Mapper 就绪 → 可开始 user story
+
+---
+
+## Phase 3: User Story 1 - 发起 OMG 支付 (Priority: P1) 🎯 MVP part 1
+
+**Goal**: C 端用户下单后调发起接口拿到 form 参数,前端 Form Post 跳转 OMG 收银台(ChoosePayment=ALL)
+
+**Independent Test**: 对已开通 OMG 的门店下单 → 调 `/pay/omg/create` → 返回 `gatewayUrl` + 全参数(含 CheckMacValue) → 前端跳转 OMG 页出现支付方式;`pos_order_omg_payment` 落一行(pay_status=0)、`pos_order.pay_type="7"`
+
+- [ ] T012 [P] [US1] 创建 `ruoyi-system/src/main/java/com/ruoyi/system/domain/dto/StoreOmgCredentialDto.java`(storeId/merchantId/hashKey/hashIv/enabledPayments,`@Valid`)与 `ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/PosStoreOmgVo.java`(列表/详情 VO)
+- [ ] T013 [US1] 实现 `ruoyi-system/.../service/IPosStoreOmgService.java` + `impl/PosStoreOmgServiceImpl.java`(纯 DB:`getOrCreateByStoreId`、`getEnabledConfig(storeId)` 返回已开通且启用的凭证或 null、`getEnabledByMerchantId(merchantId)` 回调按商店代号反查凭证),仿 PosStoreEzpayServiceImpl
+- [ ] T014 [US1] 实现 `ruoyi-system/.../service/IPosOrderOmgPaymentService.java` + `impl/PosOrderOmgPaymentServiceImpl.java`:`createPayment(ddId,merchantTradeNo,storeId,merchantId,amount,choosePayment)`、`getByTradeNo`、`getByMerchantTradeNo`、`getLatestByDdId`、`markSuccess(...payType,rtnCode,rtnMsg,authCode,payTime,callbackRaw)`(幂等:tradeNo 已 pay_status=1 返回 0)、`markFail`、`markRefunded`
+- [ ] T015 [US1] 在 `ruoyi-system/.../domain/PosOrder.java` 附近或新建常量类定义 `PAY_TYPE_OMG="7"`(OMG 在线支付),不改表结构
+- [ ] T016 [US1] 创建 `ruoyi-admin/src/main/java/com/ruoyi/app/pay/OmgPayController.java` 实现 `POST /pay/omg/create`(`@Anonymous`+`@Auth`,Header token):校验订单存在/未支付/属当前用户/金额>0 → `getEnabledConfig(mdId)`(无则 `error("该门店暂不支持线上支付")`)→ `MerchantTradeNo="OMG"+清洗(ddId)` → 组参(MerchantID/MerchantTradeNo/MerchantTradeDate=yyyy/MM/dd HH:mm:ss/PaymentType=aio/TotalAmount=amount/ReturnURL/ChoosePayment=ALL/EncryptType=1/ItemName/InvoiceMark=N)→ `OmgPay.createAioForm` 生成 CheckMacValue 与 form 字段 → `createPayment` 落流水(pay_status=0) → 更新 `pos_order.pay_type="7"`/`pay_url=gatewayUrl` → 返回 form 字段(见 contracts/api.md B1)
+- [ ] T017 [US1] 前端 `E:\QtwCode\foodie\foodie-store\` 新增「OMG 支付」入口:下单后调 `/pay/omg/create`,用返回字段构建隐藏 form 自动 submit 到 `gatewayUrl`;支付相关文案用 `$t()` 四语 i18n(zh/tw/en/vi,key 加到对应对象层级,有意义命名)
+
+**Checkpoint**: 发起支付链路通,可跳转 OMG 测试收银台(此时回调未接,订单状态待 US2 更新)
+
+---
+
+## Phase 4: User Story 2 - 支付回调核销与履约触发 (Priority: P2) 🎯 MVP part 2
+
+**Goal**: OMG `ReturnURL` 回调到达后,验签 CheckMacValue + 幂等 + 金额 + RtnCode/SimulatePaid 校验,更新订单 payStatus=1 并触发推送(含可接单骑手);`/pay/omg/return` 仅引导
+
+**Independent Test**: US1 发起并跳转后用测试卡付款 → OMG 回调 `/pay/omg/notify` → 订单 payStatus=1、`pos_order_omg_payment` 写 trade_no/pay_type=`Credit_CreditCard`/rtn_code=1、用户/商家/骑手收到推送;重复回调不变;金额不符/伪造签名拒绝
+
+- [ ] T018 [US2] 在 `OmgPayController.java` 实现 `POST /pay/omg/notify`(`@Anonymous`):collectForm 收参 → 记 IpnLog → 由 `MerchantID` 调 `getEnabledByMerchantId` → 无凭证记录并返回
+- [ ] T019 [US2] notify 验签:`OmgCheckMacValue.verify(全部回调参, hashKey, hashIv)`,与回传 `CheckMacValue` 不符 → 拒绝并记录(绝不变更订单),仍回 `1|OK`
+- [ ] T020 [US2] notify 幂等+金额+状态校验:按 `TradeNo` 查 `pos_order_omg_payment`(已 pay_status=1 → 直接返回 `1|OK`);`TradeAmt==订单 amount`(不符→拒绝核销、告警);由 `MerchantTradeNo` 反查流水与订单(不存在→记录待查返回);判 `RtnCode==1 且 SimulatePaid!=1`(模拟支付 `SimulatePaid=1` → 不发货、记录)
+- [ ] T021 [US2] notify 成功业务链路:`markSuccess`(pay_type=回覆 PaymentType 如 `Credit_CreditCard`、rtn_code、pay_time=PaymentDate、callback_raw)→ 更新 `pos_order`(state=0, payStatus=1) → `orderLogHelper.logSync` 记日志 → `PayPush`+`pushEventService.PublisherEvent` 推送用户/商家(参照 `PosOrderController.sendHdfkMessage` 货到付款同款 `OrderPushBodyDto.getJson`)→ **补全可接单骑手推送**:在 `OmgPayController` 内独立实现(复用 `PayPush.sendAcceptRiderPush` / 货到付款链路调用方式,**不复用 NewebPay 代码**,见 CLAUDE.md「sendAcceptRiderPush 迁移」)→ 回应**纯字符串 `1|OK`**(注意:非 JSON,与蓝新不同)
+- [ ] T022 [US2] notify 失败处理(`RtnCode!=1`):`markFail` 写流水(pay_status=2)、订单保持未支付、记 `rtn_code`+`rtn_msg`(不硬编码错误码分支,见 contracts §C3)、回 `1|OK`
+- [ ] T023 [US2] 在 `OmgPayController.java` 实现 `GET|POST /pay/omg/return`(`@Anonymous`):仅 302 重定向前端结果页(带 ddId),**不改订单状态**(以 notify 为准)
+- [ ] T024 [US2] 前端 `E:\QtwCode\foodie\foodie-store\` 支付结果页:展示支付状态、轮询订单 payStatus(ReturnURL 不改状态,以轮询/回调后状态为准);文案四语 i18n
+
+**Checkpoint**: US1+US2 构成完整 MVP——下单→付款→订单已支付→推送,可端到端验证(quickstart 场景 B、C)
+
+---
+
+## Phase 5: User Story 5 - 门店 OMG 凭证开通管理 (Priority: P2)
+
+**Goal**: 运营在平台后台录入/验证/启停门店 OMG 凭证(独立于蓝新凭证表)
+
+**Independent Test**: 后台为门店录入测试凭证 → 调 `QueryTradeInfo/V5` 验证通过 → 状态已开通 → 该门店可发起支付;停用后不可
+
+- [ ] T025 [US5] 创建 `ruoyi-admin/src/main/java/com/ruoyi/app/mendian/PosStoreOmgController.java`(`/system/storeOmg`,`@PreAuthorize("chanting:storeOmg:*")`):`list`/`{storeId}`/`apply/{storeId}`/`saveCredentials`/`toggleEnable/{storeId}`/`reset/{storeId}`/`enabledPayments/{storeId}`,仿 `PosStoreEzpayController` 模式
+- [ ] T026 [US5] `saveCredentials` 凭证验证:Controller 调 `OmgPay.queryTrade(fictitious MerchantTradeNo)` 探测;响应提示金钥/商店错误 → 视为无效拒绝、状态不变、记 `last_verify_result`;否则 `enableWithCredentials`(omg_status=2/is_enabled=1)
+- [ ] T027 [US5] 扩展 `PosStoreOmgServiceImpl`:`apply`(0→1)、`enableWithCredentials`、`toggleEnable`、`reset`、`setEnabledPayments`、`recordVerifyResult`(仿 PosStoreEzpayServiceImpl 状态机)
+- [ ] T028 [US5] 平台后台 `E:\QtwCode\foodie\foodie-admin-vue\` 新增门店 OMG 凭证管理页(列表分页/详情/录入凭证/启停/重置/设置方式),文案四语 i18n
+- [ ] T029 [US5] 菜单与权限:新增菜单项 + 权限标识 `chanting:storeOmg:list/query/apply/saveCredentials/toggleEnable`,`sys_menu` 插入 SQL 写入 `updatesql/sql.md`(仿 storeEzpay 菜单,不直接执行)
+
+**Checkpoint**: 凭证可全流程管理,US1 发起支付有真实凭证来源(不必手插数据)
+
+---
+
+## Phase 6: User Story 3 - ATM / 超商 非即时支付 (Priority: P3)
+
+**Goal**: `ChoosePayment=ALL` 下消费者选 ATM/超商时,平台展示 OMG 虚帐/缴费码,消费者期限内付款后回调核销
+
+**Independent Test**: 选 ATM/超商 → OMG 回调 `/pay/omg/paymentInfo` 带虚帐/缴费码 → 平台展示 + 期限 → 消费者付款 → `/pay/omg/notify`(RtnCode=1) → 订单核销(quickstart 场景 D)
+
+- [ ] T030 [US3] 在 `OmgPayController.java` 实现 `POST /pay/omg/paymentInfo`(`@Anonymous`):`OmgCheckMacValue.verify` → 解析 BankCode/vAccount/ExpireDate 或 PaymentNo/ExpireDate → 落 `pos_order_omg_payment.callback_raw`(并可用专用字段/`pay_url` 存取号码供前端读)→ 回 `1|OK`
+- [ ] T031 [US3] 暴露取号信息给前端:`GET /pay/omg/paymentInfo/{orderid}`(或复用订单查询)返回虚帐/缴费码 + 期限;前端 `E:\QtwCode\foodie\foodie-store\` ATM/超商结果页展示 + 倒计时;文案四语 i18n
+- [ ] T032 [US3] 延期付款最终核销:消费者实际付款后 OMG 回调 `/pay/omg/notify`(RtnCode=1)→ 复用 US2 的 `markSuccess` + 推送链路(pay_type=`ATM_*`/`CVS_*`)→ 订单已支付
+
+**Checkpoint**: ATM/超商 全链路(取号→展示→付款→核销)
+
+---
+
+## Phase 7: User Story 4 - 订单取消与退款 (Priority: P3)
+
+**Goal**: 订单取消时,信用卡订单调 OMG `DoAction` 退款并同步状态;ATM/超商记录待人工
+
+**Independent Test**: 对一笔 OMG 已支付信用卡订单发起取消 → 调 `DoAction`(Action=R) → `pos_order_omg_refund` 记录、`pos_order_omg_payment.pay_status=3`、`pos_order.pay_status=2`;失败→记 `rtn_msg` 可重试(quickstart 场景 E)
+
+- [ ] T033 [US4] 实现 `ruoyi-system/.../service/IPosOrderOmgRefundService.java` + `impl`(`record(paymentId,ddId,tradeNo,action,amount,rtnCode,rtnMsg,callbackRaw)`、`listByPayment`、`listByDdId`)
+- [ ] T034 [US4] 在 `OmgPayController.java` 实现退款(`POST /pay/omg/refund?orderid=` 或由取消链路调用):查 OMG 已支付订单 → 信用卡(pay_type=`Credit_CreditCard`) → `OmgPay.doAction(Action=R, TotalAmount=amount)` → 写 `pos_order_omg_refund` → `RtnCode==1` 则 `markRefunded`(pay_status=3) + `pos_order.pay_status=2`;`ATM_*/CVS_*/BarcodeATM_*` 无 API → 记录待人工处理
+- [ ] T035 [US4](可选/增强)状态化 Action:下单时 `NeedExtraPaidInfo=Y` 取 `gwsr`,退款前查 `CreditDetail/QueryTrade/V2`(需盘合后台 `CreditCheckCode`) 决定 `N(已授權)/E(要關帳)/R(已關帳)`;MVP 可先统一 `R`,失败再分支
+- [ ] T036 [US4] 接入订单取消链路:`PosOrderShOprateController`(商家取消) / `UserOrderController`(用户取消) 取消 OMG 已支付订单时触发退款;相关提示文案四语 i18n
+
+**Checkpoint**: 信用卡退款闭环(stage `DoAction` 不可用,退款须正式小额或 mock 验证)
+
+---
+
+## Phase 8: Polish & Cross-Cutting
+
+**Purpose**: 多 story 共享的收尾与护栏
+
+- [ ] T037 [P] 四语 i18n 统一校验:foodie-store 与 foodie-admin-vue 的 zh.js/tw.js/en.js/vi.js 中 OMG 支付相关 key 四文件齐全、命名有意义、位于正确对象层级(用 i18n-consistency-checker 或人工核)
+- [ ] T038 [P] 安全复核:`/pay/omg/notify`、`/pay/omg/paymentInfo`、`/pay/omg/return` 确为 `@Anonymous` 且仅这些匿名;HashKey/HashIV 不出现在任何前端响应/日志明文;CheckMacValue 验签为强制前置;金额校验与 `SimulatePaid` 守卫不可绕过
+- [ ] T039 [P] **零蓝新依赖复核**:grep `com.ruoyi.app.utils.omg` 及 `OmgPayController`/`PosStoreOmg*`/`PosOrderOmg*` 全部 OMG 新建类,确认无任何 `newebpay` import(必须为 0,违反硬约束)
+- [ ] T040 运行 `specs/016-omg-payment/quickstart.md` 全部场景 A–E 端到端验证(stage + 测试卡 + 内网穿透),记录结果(退款场景 E 因 stage 不可用需正式小额/mock)
+- [ ] T041 文档一致性:确认 spec.md/plan.md/tasks.md 描述一致;`updatesql/sql.md` 中 SQL 齐全(三建表 + OMG 菜单权限)
+
+---
+
+## Dependencies & Execution Order
+
+### Phase 依赖
+- **Phase 1 Setup**: 无依赖,立即开始
+- **Phase 2 Foundational**: 依赖 Phase 1 —— **阻塞所有 user story**(`OmgCheckMacValue` 自测必须先过)
+- **Phase 3 US1**: 依赖 Phase 2
+- **Phase 4 US2**: 依赖 US1(回调处理依赖发起产生的流水与订单关联)
+- **Phase 5 US5(凭证管理)**: 依赖 Phase 2(凭证表/Mapper);**可与 US1/US2 并行**(US1 测试可临时手插凭证数据)
+- **Phase 6 US3(ATM/超商)**: 依赖 US1+US2(复用发起+回调主链路,增量在取号回调与展示)
+- **Phase 7 US4(退款)**: 依赖 US1+US2(依赖已支付流水与 trade_no)
+- **Phase 8 Polish**: 依赖所有欲交付的 story 完成
+
+### User Story 独立性
+- **US1**: 依赖 Foundational,无跨 story 依赖
+- **US2**: 依赖 US1(闭环下半段)
+- **US3**: 依赖 US1+US2,增量在 PaymentInfoURL 取号与展示
+- **US4**: 依赖 US1+US2
+- **US5**: 依赖 Foundational,可与 US1/US2 并行
+
+### 并行机会
+- Phase 2 的 T003/T005/T006/T007/T008/T009/T010/T011 互不冲突,可全部并行(T004 CheckMacValue 优先跑通自测)
+- US5(Phase 5)可与 US1/US2(Phase 3/4)并行(不同文件、不同端)
+- Phase 8 的 T037/T038/T039 可并行
+
+---
+
+## Parallel Example: Foundational
+
+```bash
+# 以下任务文件互不冲突,可并行:
+Task: "OmgPayConfig in ruoyi-admin/.../utils/omg/OmgPayConfig.java"
+Task: "PosStoreOmg in ruoyi-system/.../domain/PosStoreOmg.java"
+Task: "PosOrderOmgPayment in ruoyi-system/.../domain/PosOrderOmgPayment.java"
+Task: "PosOrderOmgRefund in ruoyi-system/.../domain/PosOrderOmgRefund.java"
+Task: "PosStoreOmgMapper + XML"
+Task: "PosOrderOmgPaymentMapper + XML"
+Task: "PosOrderOmgRefundMapper + XML"
+Task: "OmgPay HTTP 客户端骨架 in ruoyi-admin/.../utils/omg/OmgPay.java"
+# OmgCheckMacValue(T004) 含被他人依赖的签名契约,建议优先完成并跑通自测
+```
+
+---
+
+## Implementation Strategy
+
+### MVP First(US1 + US2)
+1. Phase 1 Setup + Phase 2 Foundational(**`OmgCheckMacValue` 自测必须先过**)
+2. Phase 3 US1(发起支付)+ Phase 4 US2(回调处理)→ 端到端验证(quickstart 场景 B、C)
+3. **STOP and VALIDATE**: 下单→信用卡付款→订单已支付→推送(含可接单骑手),全链路通
+4. 此时已是最小可用产品
+
+### 增量交付
+1. MVP(US1+US2)→ 验证 → 可上测试环境
+2. + US5(凭证管理)→ 去除手插数据、运营自助
+3. + US3(ATM/超商)→ 覆盖延期方式
+4. + US4(退款)→ 售后资金闭环
+5. Polish(i18n/安全/零蓝新复核/全场景验证)→ 可转正式
+
+### 并行策略
+- Foundational 完成后:开发者 A 做 US1+US2(主链路),开发者 B 做 US5(凭证管理前后端),互不阻塞
+- US3/US4 待主链路稳定后接力
+
+---
+
+## Notes
+
+- [P] 任务 = 不同文件、无依赖;同文件多改动须串行
+- 前端 CRLF 文件(foodie-store/foodie-admin-vue)用 Python 脚本编辑,避免 Edit 匹配失败
+- 所有数据库变更写 `updatesql/sql.md`,不直接执行
+- 每个任务或逻辑组完成后提交(按用户指示);在 test 分支开发,不新建分支
+- HashKey/HashIV 为敏感凭证:仅后端持有,日志脱敏,不下发前端
+- **硬约束**:全程零 `newebpay` 依赖(T039 复核);Apple Pay 归在 Credit 下,回覆 PaymentType=`Credit_CreditCard`

Некоторые файлы не были показаны из-за большого количества измененных файлов