Ver código fonte

项目关键模块文档说明以及前端工程规则说明文件

skyline 1 mês atrás
pai
commit
7a93b0b636

+ 246 - 0
docs/分销与邀请模块.md

@@ -0,0 +1,246 @@
+# 分销与邀请模块业务说明
+
+> 本文档描述分销体系(合作商管理、佣金、提现)和邀请有礼活动的完整链路。
+> 帮助开发人员快速理解"为什么这样设计"以及各环节的特殊处理方式。
+
+---
+
+## 一、分销体系概览
+
+```
+合作商 (Distributor)
+    ├── 推荐用户 (DistributorReferral)
+    │     └── 用户下单 → 产生佣金 (DistributorCommission)
+    ├── 佣金余额管理
+    │     ├── commissionBalance(可用余额)
+    │     ├── frozenAmount(冻结金额)
+    │     ├── totalCommission(累计佣金)
+    │     └── totalWithdrawn(已提现总额)
+    ├── 提现管理 (DistributorWithdrawal)
+    └── 月度报表 (DistributorMonthlyReport)
+```
+
+---
+
+## 二、合作商管理
+
+### 2.1 Distributor 实体
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | Long | 雪花算法主键(`@JsonSerialize(using = ToStringSerializer.class)`) |
+| code | String | 合作商编码(唯一标识) |
+| name | String | 合作商名称 |
+| phone | String | 联系电话 |
+| status | Integer | 状态 |
+| qrcodeContent | String | 推广二维码内容 |
+| totalReferrals | Integer | 总推荐人数 |
+| validReferrals | Integer | 有效推荐人数 |
+| commissionBalance | BigDecimal | 可用佣金余额 |
+| frozenAmount | BigDecimal | 冻结金额 |
+| totalCommission | BigDecimal | 累计佣金总额 |
+| totalWithdrawn | BigDecimal | 已提现总额 |
+| remark | String | 备注 |
+| creatorId | Long | 创建人ID |
+| creatorName | String | 创建人姓名 |
+| deleted | Integer | 逻辑删除 |
+
+### 2.2 DistributorConfig(合作商配置)
+
+| 字段 | 说明 |
+|------|------|
+| commissionRate | 佣金比例 |
+| minWithdrawAmount | 最低提现金额 |
+| commissionFreezeDays | 佣金冻结天数 |
+
+---
+
+## 三、推荐与佣金
+
+### 3.1 DistributorReferral(推荐记录)
+
+| 关键字段 | 说明 |
+|---------|------|
+| distributorId | 合作商ID |
+| userId | 被推荐用户ID |
+| status | 推荐状态 |
+| sourceChannel | 来源渠道 |
+| firstOrderAmount | 首单金额 |
+| firstOrderTime | 首单时间 |
+
+### 3.2 DistributorCommission(佣金记录)
+
+| 关键字段 | 说明 |
+|---------|------|
+| distributorId | 合作商ID |
+| orderId | 订单ID |
+| referralId | 推荐记录ID |
+| orderAmount | 订单金额 |
+| commissionRate | 佣金比例 |
+| commissionAmount | 佣金金额 |
+| status | 状态:0-待结算 1-已结算 2-已冻结 3-已取消 |
+
+### 3.3 佣金产生流程
+
+```
+被推荐用户下单 → 订单完成(payStatus=PAID)
+    ↓
+检查该用户是否有推荐记录(DistributorReferral)
+    ↓
+计算佣金 = 订单金额 × 佣金比例
+    ↓
+创建佣金记录(DistributorCommission,status=待结算)
+    ↓
+佣金冻结期过后 → 状态变为已结算
+    ↓
+更新合作商 commissionBalance
+```
+
+#### 为什么佣金有冻结期?
+
+订单可能发生退款,如果在退款前佣金已结算并提现,会造成资金损失。冻结期(`commissionFreezeDays`)确保订单度过退款风险期后佣金才变为可用,这是分销业务的标准风控手段。
+
+冻结期由 `DistributorConfig.commissionFreezeDays` 配置,不同合作商可设置不同天数。
+
+---
+
+## 四、提现管理
+
+### 4.1 DistributorWithdrawal(提现记录)
+
+| 关键字段 | 说明 |
+|---------|------|
+| distributorId | 合作商ID |
+| amount | 提现金额 |
+| status | 状态:0-待审核 1-已通过 2-已拒绝 3-已打款 |
+| bankName | 收款银行 |
+| bankAccount | 收款账号 |
+| accountName | 收款人姓名 |
+| reviewRemark | 审核备注 |
+| reviewerId | 审核人ID |
+| reviewTime | 审核时间 |
+
+### 4.2 提现流程
+
+```
+合作商申请提现
+    ↓
+检查:余额 >= 提现金额 >= 最低提现额度
+    ↓
+冻结金额(frozenAmount += amount)
+    ↓
+创建提现记录(status=待审核)
+    ↓
+运营平台审核
+    ├── 通过 → 打款 → 状态变为已打款
+    │         → 扣减 commissionBalance 和 frozenAmount
+    │         → 累加 totalWithdrawn
+    └── 拒绝 → 解冻(frozenAmount -= amount)→ 状态变为已拒绝
+```
+
+#### 提现审核的合规原因
+
+分销业务涉及资金流出,需要人工审核确认:
+- 防止恶意刷单后提现
+- 确认银行账户信息正确
+- 满足财务合规要求
+
+提现冻结机制确保审核期间这部分金额不会被二次提现。
+
+---
+
+## 五、月度报表
+
+### 5.1 DistributorMonthlyReport
+
+| 关键字段 | 说明 |
+|---------|------|
+| distributorId | 合作商ID |
+| year / month | 年月 |
+| totalOrders | 月度总订单数 |
+| totalOrderAmount | 月度总订单金额 |
+| totalCommission | 月度总佣金 |
+| newReferrals | 月度新增推荐人数 |
+| validReferrals | 月度有效推荐人数 |
+
+---
+
+## 六、小程序端分销接口
+
+| 接口 | 控制器 | 说明 |
+|------|--------|------|
+| 获取合作商信息 | DistributorMpController | 合作商个人中心 |
+| 获取推荐列表 | DistributorMpController | 推荐用户列表 |
+| 获取佣金记录 | DistributorMpController | 佣金明细 |
+| 申请提现 | DistributorMpController | 发起提现 |
+| 获取提现记录 | DistributorMpController | 提现历史 |
+| 生成推广码 | DistributorMpController | 推广二维码 |
+
+---
+
+## 七、邀请有礼体系
+
+### 7.1 与分销的区别
+
+| 维度 | 分销(Distributor) | 邀请有礼(InviteActivity) |
+|------|---------------------|---------------------------|
+| 角色 | 合作商(B端) | 普通用户(C端) |
+| 奖励 | 佣金(金额) | 优惠券 |
+| 管理 | 运营平台分配 | 用户自主参与 |
+| 触发 | 被推荐人下单 | 被邀请人首单/绑定 |
+
+#### 为什么邀请有礼发优惠券而非佣金?
+
+分销面向B端合作商,发佣金合理且合规。邀请有礼面向C端普通用户,发佣金涉及个人税务和支付通道问题,而发优惠券:
+- 无合规风险
+- 促进复购(优惠券只能在平台消费)
+- 实现成本低(无需对接提现通道)
+- 双向奖励(邀请人和被邀请人各得一张券)更容易实施
+
+### 7.2 邀请流程
+
+```
+邀请人分享邀请码/链接
+    ↓
+被邀请人扫码 → 绑定邀请关系(InviteRecord,status=待激活)
+    ↓
+被邀请人完成首单(满足 minOrderAmount 条件)
+    ↓
+InviteActivityServiceImpl 处理邀请激活
+    ↓
+更新 InviteRecord.status = 已激活/已奖励
+    ↓
+发放奖励:
+    ├── 邀请人 → 发放 couponTemplateId 对应的优惠券
+    └── 被邀请人 → 发放 inviteeCouponTemplateId 对应的优惠券(双向奖励时)
+```
+
+### 7.3 邀请限制
+
+- 每日邀请上限:dailyLimit
+- 活动期间总上限:totalLimit
+- 首单金额门槛:requireFirstOrder + minOrderAmount
+- 防重复:同一被邀请人只能被同一邀请人邀请一次
+
+---
+
+## 八、核心代码文件索引
+
+| 文件 | 职责 |
+|------|------|
+| `haha-entity/.../Distributor.java` | 合作商实体 |
+| `haha-entity/.../DistributorCommission.java` | 佣金记录实体 |
+| `haha-entity/.../DistributorConfig.java` | 合作商配置实体 |
+| `haha-entity/.../DistributorReferral.java` | 推荐记录实体 |
+| `haha-entity/.../DistributorWithdrawal.java` | 提现记录实体 |
+| `haha-entity/.../DistributorMonthlyReport.java` | 月度报表实体 |
+| `haha-entity/.../InviteActivity.java` | 邀请活动实体 |
+| `haha-entity/.../InviteRecord.java` | 邀请记录实体 |
+| `haha-entity/.../InviteReward.java` | 邀请奖励实体 |
+| `haha-service/.../DistributorServiceImpl.java` | 合作商服务 |
+| `haha-service/.../DistributorCommissionServiceImpl.java` | 佣金服务 |
+| `haha-service/.../DistributorWithdrawalServiceImpl.java` | 提现服务 |
+| `haha-service/.../DistributorReportServiceImpl.java` | 报表服务 |
+| `haha-service/.../InviteActivityServiceImpl.java` | 邀请活动服务(含激活逻辑) |
+| `haha-admin/.../DistributorAdminController.java` | 运营平台-合作商API |
+| `haha-miniapp/.../InviteController.java` | 小程序-邀请API |

+ 334 - 0
docs/系统架构与公共模块.md

@@ -0,0 +1,334 @@
+# 系统架构与公共模块说明
+
+> 本文档描述项目整体架构、双应用部署、模块依赖、公共组件及开发规范要点。
+> 帮助开发人员快速理解“为什么这样设计”以及各环节的特殊处理方式。
+
+---
+
+## 一、系统架构
+
+### 1.1 双应用架构
+
+| 应用 | 模块 | 端口 | Context-Path | 用途 |
+|------|------|------|--------------|------|
+| AdminApplication | haha-admin | 7070 | /admin | 运营平台后端 |
+| MiniappApplication | haha-miniapp | 7077 | /api | 小程序后端 |
+
+两个应用共享 `haha-common` / `haha-entity` / `haha-mapper` / `haha-service`,通过 `@ComponentScan(basePackages = {"com.haha"})` 和 `@MapperScan("com.haha.mapper")` 自动扫描。
+
+#### 为什么采用双应用而非单应用?
+
+运营平台(haha-admin)和小程序后端(haha-miniapp)的**认证方式、API风格、安全要求**完全不同:
+- 运营平台:Sa-Token 管理员登录,权限控制严格(@RequirePermission),仅运营人员访问
+- 小程序:微信登录 + accessToken,面向所有用户,无权限注解
+
+分离部署的优势:
+- 独立扩缩容:小程序端流量远大于运营端
+- 安全隔离:运营平台不暴露给用户端
+- 独立发布:两端可独立上线,互不影响
+
+### 1.2 模块依赖方向(严格单向)
+
+```
+haha-admin  -->  haha-service  -->  haha-mapper  -->  haha-entity
+haha-miniapp -->  haha-service  -->  haha-mapper  -->  haha-entity
+                                        |
+haha-common <---------------------------+(所有模块均可依赖)
+haha-sdk(独立模块,不依赖其他内部模块)
+```
+
+**禁止反向依赖**:
+- haha-mapper 不可依赖 haha-service
+- haha-entity 不可依赖 haha-mapper 或 haha-service
+- haha-common 不可依赖任何其他内部模块
+- haha-admin 与 haha-miniapp 不可互相依赖
+
+### 1.3 技术栈版本
+
+| 技术 | 版本 | 说明 |
+|------|------|------|
+| Java | 21 | 禁止使用 Java 22+ 特性 |
+| Spring Boot | 4.0.3 | 使用 Jakarta 命名空间 |
+| MyBatis-Plus | 3.5.16 | mybatis-plus-spring-boot4-starter |
+| Sa-Token | 1.45.0 | sa-token-spring-boot3-starter |
+| Hutool | 5.8.40 | 禁止使用 hutool 6.x API |
+| FastJson2 | 2.0.53 | 禁止使用 fastjson (v1) |
+| Lombok | 1.18.36 | -- |
+| Jakarta EE | Jakarta | 非 Javax |
+
+---
+
+## 二、前端应用
+
+### 2.1 运营平台(haha-admin-web)
+
+- 框架:Vue 3 + Element Plus + TypeScript
+- 构建:Vite
+- 状态管理:Pinia
+- 路由:Vue Router
+
+### 2.2 用户小程序(haha-mp)
+
+- 框架:UniApp + Vue 3 + TypeScript
+- 构建:Vite
+- 微信登录 + 支付分集成
+
+### 2.3 管理端小程序(haha-admin-mp)
+
+- 框架:UniApp + Vue 3 + TypeScript
+- 用途:合作商管理、门店巡检等
+
+---
+
+## 三、公共模块 (haha-common)
+
+### 3.1 枚举类 (`com.haha.common.enums`)
+
+| 枚举 | 说明 | code类型 |
+|------|------|----------|
+| OrderStatus | 订单状态 | int |
+| PayStatus | 支付状态 | String |
+| PaymentChannel | 支付渠道 | String |
+| DeviceDoorStatus | 门状态 | String |
+| DeviceAlertType | 告警类型 | String |
+| ActivityTypeEnum | 活动类型 | int |
+| ActivityStatusEnum | 活动状态 | int |
+| CouponTypeEnum | 优惠券类型 | int |
+| CouponStatusEnum | 优惠券状态 | int |
+| DiscountTypeEnum | 折扣类型 | int |
+| ValidTypeEnum | 有效期类型 | int |
+| DistributeTypeEnum | 发放类型 | int |
+| ApplyScopeEnum | 适用范围 | int |
+| OpenType | 开门类型 | String |
+| InviteStatusEnum | 邀请状态 | int |
+| InviteRewardStatusEnum | 邀请奖励状态 | int |
+| PayScoreState | 支付分状态 | String |
+
+**枚举规范**:
+- 只含 `code` + `description/label` 字段
+- 必须提供 `fromCode()` / `getByCode()` 静态方法
+- 数据库存储 `code` 值,禁止存储 `description`
+
+### 3.2 常量类 (`com.haha.common.constant`)
+
+| 常量类 | 说明 |
+|--------|------|
+| OrderConstants | 订单状态/支付状态/订单类型常量 |
+| DeviceConstants | 设备相关常量 |
+| MarketingConstants | 营销相关常量 |
+| RedisConstants | Redis Key 定义 |
+| CallbackConstants | 回调类型常量 |
+| CommonConstants | 通用常量 |
+| ResponseEnum | 业务响应码枚举 |
+
+### 3.3 通用VO
+
+| VO | 说明 |
+|----|------|
+| Result\<T\> | 统一返回封装(code/message/data) |
+| PageResult\<T\> | 分页返回(通过 `PageResult.of(IPage<T>)` 转换) |
+| StatusLabel | 状态标签(label + color) |
+| OrderVO | 订单详情VO |
+
+### 3.4 统一返回格式
+
+```json
+{
+  "code": 200,
+  "message": "操作成功",
+  "data": { ... }
+}
+```
+
+| 场景 | code | message |
+|------|------|---------|
+| 成功 | 200 | 操作成功 / 具体描述 |
+| 参数错误 | 400 | 字段错误描述 |
+| 未登录 | 401 | 未登录 / token已过期 |
+| 无权限 | 403 | 无访问权限 |
+| 资源不存在 | 404 | xxx不存在 |
+| 业务异常 | 自定义 | BusinessException 消息 |
+| 系统异常 | 500 | 系统繁忙,请稍后重试 |
+
+---
+
+## 四、哈哈SDK (haha-sdk)
+
+### 4.1 用途
+
+独立模块,封装与哈哈平台(智能售卖机设备平台)的API交互,不依赖其他内部模块。
+
+### 4.2 核心功能
+
+| 功能 | 说明 |
+|------|------|
+| 设备API | 开门、查询状态、设置温度/音量 |
+| 商品API | 商品查询、同步 |
+| 订单API | 创建订单、查询订单 |
+| 回调验证 | 签名校验 |
+
+### 4.3 认证方式
+
+通过 `authToken` 进行设备级认证,SDK内部处理签名和请求构建。
+
+---
+
+## 五、权限体系
+
+### 5.1 运营平台权限(Sa-Token)
+
+- 权限注解:`@RequirePermission("module:operation")`
+- operation 标准值:`read` / `create` / `update` / `delete` / `other`
+- 小程序端不需要 `@RequirePermission`(通过 accessToken 鉴权)
+
+### 5.2 数据权限 (`DataPermission`)
+
+支持按门店维度的数据隔离,运营人员只能查看授权门店的数据。
+
+### 5.3 操作日志
+
+- 注解:`@Log(module = "模块名", operation = OperationType.XXX, summary = "描述")`
+- OperationType:CREATE / UPDATE / DELETE / OTHER
+- 读操作不加 `@Log`
+
+---
+
+## 六、数据库规范
+
+### 6.1 表命名
+
+- 前缀 `t_`,如 `t_order`、`t_device`
+- 关联表:`t_{主实体}_{关联实体}`,如 `t_activity_device`、`t_coupon_shop`
+
+### 6.2 ID策略
+
+- 全局雪花算法:`@TableId(type = IdType.ASSIGN_ID)`
+- 数据库列类型:BIGINT
+- 禁止自增ID
+
+### 6.3 字段命名
+
+- 数据库列:下划线(create_time, pay_status)
+- Java字段:驼峰(createTime, payStatus)
+- 自动映射:`map-underscore-to-camel-case: true`
+
+### 6.4 XML映射文件
+
+- 位置:`haha-mapper/src/main/resources/mapper/XxxMapper.xml`
+- 必须定义 `resultMap`,禁止 `resultType`
+- 逻辑删除不会自动添加到手写SQL中
+
+---
+
+## 七、数据库表总览
+
+### 核心业务表
+
+| 表名 | 说明 |
+|------|------|
+| t_order | 订单表 |
+| t_order_goods | 订单商品明细 |
+| t_order_item | 订单商品项 |
+| t_device | 设备表 |
+| t_shop | 门店表 |
+| t_product | 商品表 |
+| t_device_inventory | 设备库存表 |
+| t_door_record | 开关门记录表 |
+| t_user | 用户表 |
+| t_account | 用户账户表 |
+
+### 营销优惠表
+
+| 表名 | 说明 |
+|------|------|
+| t_marketing_activity | 营销活动表 |
+| t_activity_shop | 活动-门店关联 |
+| t_activity_device | 活动-设备关联 |
+| t_activity_product | 活动-商品关联 |
+| t_coupon_template | 优惠券模板表 |
+| t_user_coupon | 用户优惠券表 |
+| t_coupon_shop | 优惠券-门店关联 |
+| t_coupon_product | 优惠券-商品关联 |
+| t_coupon_distribute | 优惠券发放记录 |
+| t_timed_discount_activity | 定时折扣活动表 |
+| t_timed_discount_device | 定时折扣-设备关联 |
+| t_timed_discount_shop | 定时折扣-门店关联 |
+| t_timed_discount_product | 定时折扣-商品关联 |
+| t_timed_discount_record | 定时折扣执行记录 |
+| t_timed_discount_statistics | 定时折扣统计 |
+| t_invite_activity | 邀请活动表 |
+| t_invite_record | 邀请记录表 |
+| t_invite_reward | 邀请奖励表 |
+| t_marketing_statistics | 营销统计表 |
+
+### 库存管理表
+
+| 表名 | 说明 |
+|------|------|
+| t_stock_record | 补货记录表 |
+| t_stock_record_item | 补货明细表 |
+| t_inventory_log | 库存变更日志 |
+| t_price_adjustment_log | 价格调整日志 |
+
+### 系统管理表
+
+| 表名 | 说明 |
+|------|------|
+| t_admin | 管理员表 |
+| t_role | 角色表 |
+| t_role_permission | 角色权限关联 |
+| t_data_permission | 数据权限表 |
+| t_operation_log | 操作日志表 |
+| t_dict_type | 字典类型表 |
+| t_dict_data | 字典数据表 |
+| t_device_alert_record | 设备告警记录 |
+| t_announcement | 公告表 |
+
+### 分销模块表
+
+| 表名 | 说明 |
+|------|------|
+| t_distributor | 合作商表 |
+| t_distributor_commission | 佣金记录表 |
+| t_distributor_config | 合作商配置表 |
+| t_distributor_referral | 推荐记录表 |
+| t_distributor_withdrawal | 提现记录表 |
+| t_distributor_monthly_report | 月度报表 |
+
+### 统计报表表
+
+| 表名 | 说明 |
+|------|------|
+| t_stat_category_daily | 品类日统计 |
+| t_stat_device_daily | 设备日统计 |
+| t_stat_product_daily | 商品日统计 |
+| t_stat_shop_daily | 门店日统计 |
+| t_stat_user_repurchase | 用户复购统计 |
+
+---
+
+## 八、层模板管理
+
+### 8.1 LayerTemplate 实体
+
+设备货架层配置模板,定义每一层的商品排列方式。
+
+### 8.2 核心功能
+
+| 功能 | 说明 |
+|------|------|
+| 模板CRUD | 创建/编辑/删除层模板 |
+| 层配置 | 每层的商品类型、数量、位置 |
+| 设备应用 | 将模板应用到指定设备 |
+
+---
+
+## 九、开发规范要点
+
+1. **Long类型序列化**:所有 Long 类型 ID 和外键字段必须添加 `@JsonSerialize(using = ToStringSerializer.class)` 防精度丢失
+2. **日期格式**:`@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")`
+3. **金额字段**:必须使用 `BigDecimal`,禁止 `double` / `float`
+4. **依赖注入**:构造器注入(`final` + `@RequiredArgsConstructor`),循环依赖时用 `@Autowired @Lazy`
+5. **事务**:写操作必须 `@Transactional(rollbackFor = Exception.class)`
+6. **分页校验**:`page < 1` 设为 1,`pageSize > 100` 设为 10
+7. **业务异常**:使用 `BusinessException` + `ResponseEnum`,禁止 Controller 中抛出未处理异常

+ 399 - 0
docs/营销与优惠模块.md

@@ -0,0 +1,399 @@
+# 营销与优惠模块业务说明
+
+> 本文档描述营销活动与优惠券体系,涵盖活动类型、优惠券生命周期、优惠计算逻辑及叠加规则。
+> 帮助开发人员快速理解"为什么这样设计"以及各环节的特殊处理方式。
+
+---
+
+## 一、营销活动体系
+
+### 1.1 三套活动体系
+
+| 体系 | 核心实体 | 管理入口 | 用途 |
+|------|---------|---------|------|
+| 营销活动 | MarketingActivity | `MarketingActivityController` | 首单立减/折扣/满减 |
+| 定时折扣 | TimedDiscountActivity | `TimedDiscountController` | 按时间段自动执行的折扣 |
+| 邀请有礼 | InviteActivity | `InviteActivityAdminController` | 邀请奖励活动 |
+
+**统一活动管理**:`UnifiedActivityService` / `UnifiedActivityController` 提供三套活动的统一查询和管理入口。
+
+#### 为什么有三套活动体系而非统一一套?
+
+三套体系虽同属"营销",但业务差异很大,强行合并会增加复杂度:
+- **营销活动**(MarketingActivity):面向设备端计算的即时优惠,有时间范围、预算控制,由运营人员配置
+- **定时折扣**(TimedDiscountActivity):按每天时段自动执行(如夜场22:00-06:00打折),有库存关联的阶梯折扣逻辑,可自动执行
+- **邀请有礼**(InviteActivity):奖励机制完全不同(发放优惠券给邀请人/被邀请人),有每日限额、首单门槛等专属逻辑
+
+`UnifiedActivityService` 在查询层面做统一聚合,但各体系保持独立的 CRUD 和业务逻辑。
+
+### 1.2 MarketingActivity 实体
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | Long | 雪花算法主键 |
+| activityName | String | 活动名称 |
+| activityType | Integer | 活动类型:1-首单立减 2-优惠折扣 3-商品满减 4-邀请有礼 |
+| activityDesc | String | 活动描述 |
+| startTime / endTime | LocalDateTime | 活动有效期 |
+| status | Integer | 状态:0-草稿 1-已发布 2-进行中 3-已暂停 4-已结束 |
+| discountValue | BigDecimal | 优惠值(立减金额/折扣率/减免金额) |
+| minAmount | BigDecimal | 最低消费金额(满减门槛) |
+| maxDiscount | BigDecimal | 最大优惠金额(折扣活动上限) |
+| applyScope | Integer | 适用范围:1-全部门店 2-指定门店 |
+| deviceScope | Integer | 设备范围:1-全部设备 2-指定设备 |
+| productScope | Integer | 商品范围:1-全部商品 2-指定商品 |
+| totalBudget / usedBudget | BigDecimal | 预算控制 |
+| deleted | Integer | 逻辑删除 |
+
+### 1.3 活动类型与计算规则
+
+#### 类型1:首单立减 (FIRST_ORDER_DISCOUNT)
+
+```
+条件:用户之前无已完成订单(isFirstOrder 判断)
+优惠 = min(discountValue, orderAmount)
+结果:可能免单(优惠 >= 订单金额)
+```
+
+#### 类型2:优惠折扣 (DISCOUNT)
+
+```
+折扣率 discountValue:0 < discountValue <= 1(如0.8表示8折)
+优惠 = orderAmount × (1 - discountValue),保留2位小数
+如有 maxDiscount:优惠 = min(计算优惠, maxDiscount)
+```
+
+#### 类型3:商品满减 (FULL_REDUCTION)
+
+```
+门槛:orderAmount >= minAmount
+满足:优惠 = discountValue(固定减免金额)
+不满足:优惠 = 0
+```
+
+### 1.4 活动状态流转 (`ActivityStatusEnum`)
+
+```
+DRAFT(0, 草稿)
+    ↓ publish
+PUBLISHED(1, 已发布) → 到达开始时间自动 → ONGOING(2, 进行中)
+    ↓ pause                              ↓ pause
+PAUSED(3, 已暂停) → resume → ONGOING    ↓ 到达结束时间
+    ↓                                    ↓
+                                       ENDED(4, 已结束)
+```
+
+状态操作权限:
+- 可编辑:仅 DRAFT
+- 可发布:仅 DRAFT
+- 可暂停:仅 ONGOING
+- 可恢复:仅 PAUSED
+- 可删除:DRAFT 或 ENDED
+
+### 1.5 关联关系表
+
+| 关联表 | 说明 |
+|--------|------|
+| t_activity_shop | 活动-门店关联(applyScope=2时) |
+| t_activity_device | 活动-设备关联(deviceScope=2时) |
+| t_activity_product | 活动-商品关联(productScope=2时) |
+
+#### 营销活动与优惠券的关系设计
+
+**关系模型**:一个营销活动可以关联多个优惠券模板(1:N),优惠券模板的 `activityId` 字段指向所属活动,但优惠券也可以独立存在(`activityId` 为空)。
+
+**设计考量**:
+- 活动和优惠券解耦,优惠券可独立发放(如日常运营券),也可归属某个活动(如春节大促券)
+- 活动详情页可展示关联的优惠券列表,便于运营人员统一管理
+- `activityId` 为可选字段,不强制绑定
+
+---
+
+## 二、定时折扣体系
+
+### 2.1 TimedDiscountActivity 实体
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | Long | 雪花算法主键 |
+| activityName | String | 活动名称 |
+| startTime / endTime | LocalTime | 折扣时段(精确到时分秒,如 22:00:00 - 06:00:00) |
+| discountType | Integer | 折扣类型:1-固定折扣 2-阶梯折扣 3-固定价格 |
+| discountValue | BigDecimal | 折扣值 |
+| minDiscountPrice | BigDecimal | 最低折扣价(底线价格) |
+| applyScope / deviceScope / productScope | Integer | 适用范围 |
+| productTypes | String | 商品分类(逗号分隔) |
+| priorityProductTypes | String | 优先折扣商品分类 |
+| minStock / maxStock | Integer | 库存范围筛选 |
+| status | Integer | 状态 |
+| autoExecute | Integer | 是否自动执行 |
+| notifyOnExecute | Integer | 执行时是否通知 |
+
+### 2.2 折扣类型 (`DiscountTypeEnum`)
+
+| code | 枚举值 | 说明 |
+|------|--------|------|
+| 1 | FIXED_DISCOUNT | 固定折扣(如统一8折) |
+| 2 | TIERED_DISCOUNT | 阶梯折扣(库存越少折扣越大) |
+| 3 | FIXED_PRICE | 固定价格(如夜场统一3元) |
+
+#### 定时折扣的业务场景
+
+定时折扣主要解决**临期/夜间清库存**问题:
+- **夜场折扣**:22:00-06:00 折扣销售,提高夜间销量
+- **临期清仓**:库存低于阈值时自动加大折扣,减少损耗
+- **固定价格**:特定时段统一定价(如夜场饮品一律3元),简化计价
+
+**阶梯折扣**是定时折扣独有的机制:根据当前库存量动态调整折扣力度(库存越少折扣越大),由 `minStock` / `maxStock` 字段定义库存区间。
+
+### 2.3 关联表
+
+| 关联表 | 说明 |
+|--------|------|
+| t_timed_discount_device | 折扣-设备关联 |
+| t_timed_discount_shop | 折扣-门店关联 |
+| t_timed_discount_product | 折扣-商品关联 |
+| t_timed_discount_record | 折扣执行记录 |
+| t_timed_discount_statistics | 折扣统计 |
+
+---
+
+## 三、优惠券体系
+
+### 3.1 CouponTemplate 实体(优惠券模板)
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | Long | 雪花算法主键 |
+| couponName | String | 优惠券名称 |
+| couponType | Integer | 类型:1-满减券 2-折扣券 3-立减券 4-商品券 |
+| couponDesc | String | 描述 |
+| discountValue | BigDecimal | 优惠值(金额/折扣率) |
+| minAmount | BigDecimal | 最低消费金额(满减门槛) |
+| maxDiscount | BigDecimal | 最大优惠金额(折扣券上限) |
+| totalCount | Integer | 发放总量 |
+| remainCount | Integer | 剩余库存 |
+| receiveLimit | Integer | 每人限领数量 |
+| validType | Integer | 有效期类型:1-固定时间 2-相对时间 |
+| validStartTime / validEndTime | LocalDateTime | 固定有效期 |
+| validDays | Integer | 相对有效天数(领取后N天有效) |
+| applyScope | Integer | 适用范围:1-全部门店 2-指定门店 |
+| productScope | Integer | 商品范围:1-全部商品 2-指定商品 |
+| activityId | Long | 关联营销活动ID(可选) |
+| status | Integer | 状态:0-禁用 1-启用 |
+| receiveType | String | 发放类型:Collect-主动领取,Release-系统发放 |
+
+### 3.2 优惠券类型与计算 (`CouponTypeEnum`)
+
+| couponType | 名称 | discountValue含义 | 计算方式 |
+|-----------|------|-------------------|---------|
+| 1 | 满减券 | 减免金额(元) | 订单金额 >= minAmount → 优惠 = discountValue |
+| 2 | 折扣券 | 折扣率(如0.8=8折) | 优惠 = orderAmount × (1 - discountValue/10),有 maxDiscount 上限 |
+| 3 | 立减券 | 抵扣金额(元) | 优惠 = discountValue(无门槛) |
+| 4 | 商品券 | 无意义 | 免费兑换指定商品,不用于金额扣减 |
+
+### 3.3 UserCoupon 实体(用户优惠券)
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | Long | 雪花算法主键 |
+| couponCode | String | 优惠券编码(唯一) |
+| templateId | Long | 优惠券模板ID |
+| userId | Long | 用户ID |
+| orderId | Long | 使用订单ID(使用后写入) |
+| status | Integer | 状态:0-未使用 1-已使用 2-已过期 |
+| receiveTime | LocalDateTime | 领取时间 |
+| useTime | LocalDateTime | 使用时间 |
+| validStartTime / validEndTime | LocalDateTime | 有效期 |
+| discountAmount | BigDecimal | 实际优惠金额(使用时记录) |
+
+### 3.4 优惠券状态 (`CouponStatusEnum`)
+
+| code | 枚举值 | 说明 |
+|------|--------|------|
+| 0 | UNUSED | 未使用 |
+| 1 | USED | 已使用 |
+| 2 | EXPIRED | 已过期 |
+
+### 3.5 关联表
+
+| 关联表 | 说明 |
+|--------|------|
+| t_coupon_shop | 优惠券-门店关联 |
+| t_coupon_product | 优惠券-商品关联 |
+| t_coupon_distribute | 优惠券发放记录 |
+
+#### 优惠券发放类型 (`receiveType`)
+
+| 类型 | 说明 |
+|------|------|
+| Collect | 主动领取:用户在小程序“领券中心”点击领取 |
+| Release | 系统发放:系统自动发放(如邀请奖励、新人礼包) |
+
+#### 优惠券库存防超卖
+
+高并发领取场景下使用 Redis 原子操作扣减库存:
+```java
+// Redis原子扣减
+String key = "coupon:stock:" + templateId;
+Long remain = redisTemplate.opsForValue().decrement(key);
+if (remain < 0) {
+    redisTemplate.opsForValue().increment(key);  // 回滚
+    return false;
+}
+```
+
+同时使用 Redis 分布式锁防止重复领取:
+```java
+String lockKey = "coupon:receive:" + userId + ":" + templateId;
+Boolean acquired = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", Duration.ofMinutes(1));
+```
+
+---
+
+## 四、优惠计算与叠加规则
+
+### 4.1 优惠叠加规则
+
+| 优惠类型 | 是否可叠加 | 计算顺序 |
+|---------|-----------|---------|
+| 多个营销活动 | 可叠加 | 设备端计算 |
+| 优惠券 | 一次仅用1张 | 服务端计算 |
+| 营销活动 + 优惠券 | 可叠加 | 先营销活动,后优惠券 |
+
+### 4.2 优惠券自动扣减流程(订单回调时触发)
+
+#### 为什么是自动扣减?
+
+智能售卖机没有“确认订单”环节,用户关门即结算。系统无法等待用户选择优惠券,因此采用自动匹配策略:优先使用最先到期的符合条件的券。
+
+**设计考量**:
+- 优先到期策略避免优惠券过期浪费,符合用户利益
+- 一次仅用1张优惠券,避免复杂叠加计算
+- 异常不阻断:优惠券计算失败时返回原金额,不影响正常订单流程
+
+```
+HahaCallbackServiceImpl.processCouponDiscount()
+    ↓
+1. 查询用户所有可用优惠券(status=0 且未过期)
+    ↓
+2. 按到期时间升序排序(优先使用最先到期的券)
+    ↓
+3. 遍历优惠券,检查适用条件:
+   - 订单金额 >= 优惠券满减门槛(minAmount)
+   - 适用范围匹配(applyScope)
+    ↓
+4. 使用第一张符合条件的优惠券
+    ↓
+5. 计算优惠金额(根据 couponType)
+    ↓
+6. 确保优惠金额 <= 订单金额(paidAmount >= 0)
+    ↓
+7. 标记优惠券为已使用(useCoupon)
+    ↓
+8. 更新订单 discountAmount / paidAmount
+    ↓
+9. 返回最终实付金额(用于支付扣费)
+```
+
+### 4.3 优惠券使用校验(UserCouponServiceImpl.useCoupon)
+
+```
+1. 验证优惠券归属(userId 匹配)
+2. 检查状态(must be UNUSED)
+3. 检查有效期(validEndTime)
+4. 更新状态为 USED,写入 orderId / useTime / discountAmount
+```
+
+### 4.4 优惠金额公式
+
+```
+最终实付 = totalAmount - discountAmount
+discountAmount = 营销活动优惠(设备端已扣减,不可见) + 优惠券优惠(服务端扣减)
+
+注意:设备端传来的 totalAmount 已包含营销活动优惠
+      服务端 discountAmount 仅记录优惠券优惠部分
+      营销活动优惠金额不单独持久化到订单表
+```
+
+#### 为什么营销活动优惠金额不持久化到订单表?
+
+营销活动优惠是**动态计算**的:同一个活动在不同订单中的优惠金额不同(如折扣率取决于订单金额),且活动配置可能随时修改。将计算结果持久化到订单表既不必要也不合理——订单创建时优惠已确定,后续活动修改不影响已有订单。
+
+如果将来需要统计营销活动优惠总额,可通过 `MarketingRuleService.calculateOrderDiscount()` 重新计算或从统计表 `t_marketing_statistics` 获取。
+
+---
+
+## 五、邀请有礼体系
+
+### 5.1 InviteActivity 实体
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | Long | 雪花算法主键 |
+| activityName / activityDesc | String | 活动名称/描述 |
+| startTime / endTime | LocalDateTime | 活动有效期 |
+| status | Integer | 状态:0-草稿 1-已发布 2-进行中 3-已暂停 4-已结束 |
+| couponTemplateId | Long | 邀请人奖励优惠券模板ID |
+| inviteeCouponTemplateId | Long | 被邀请人奖励优惠券模板ID(双向奖励) |
+| dailyLimit | Integer | 每人每日最大邀请数 |
+| totalLimit | Integer | 每人活动期间最大邀请总数 |
+| requireFirstOrder | Integer | 是否要求完成首单:0-否 1-是 |
+| minOrderAmount | BigDecimal | 首单最低金额要求 |
+| inviteCodePrefix | String | 邀请码前缀 |
+| applyScope / productScope | Integer | 适用范围 |
+
+### 5.2 邀请记录 (InviteRecord)
+
+| 关键字段 | 说明 |
+|---------|------|
+| inviterUserId | 邀请人 |
+| inviteeUserId | 被邀请人 |
+| inviteCode | 邀请码 |
+| status | 0-待激活 1-已激活 2-已失效 3-已奖励 |
+
+### 5.3 邀请奖励 (InviteReward)
+
+| 关键字段 | 说明 |
+|---------|------|
+| rewardType | 0-邀请人奖励 1-被邀请人奖励 |
+| triggerType | 0-首单完成触发 1-邀请绑定触发 |
+| couponTemplateId | 奖励的优惠券模板 |
+| userCouponId | 发放的用户优惠券ID |
+| status | 0-待发放 1-已发放 2-发放失败 |
+
+### 5.4 邀请激活触发
+
+订单完成时(`payStatus = PAID`),`InviteActivityServiceImpl` 检查被邀请人是否有待激活的邀请记录,满足条件后发放奖励优惠券。
+
+---
+
+## 六、核心代码文件索引
+
+| 文件 | 职责 |
+|------|------|
+| `haha-entity/.../MarketingActivity.java` | 营销活动实体 |
+| `haha-entity/.../TimedDiscountActivity.java` | 定时折扣活动实体 |
+| `haha-entity/.../InviteActivity.java` | 邀请有礼活动实体 |
+| `haha-entity/.../CouponTemplate.java` | 优惠券模板实体 |
+| `haha-entity/.../UserCoupon.java` | 用户优惠券实体 |
+| `haha-service/.../MarketingRuleServiceImpl.java` | 营销规则计算(首单/折扣/满减) |
+| `haha-service/.../MarketingActivityServiceImpl.java` | 营销活动CRUD |
+| `haha-service/.../UserCouponServiceImpl.java` | 优惠券领取/使用/查询 |
+| `haha-service/.../CouponTemplateServiceImpl.java` | 优惠券模板管理 |
+| `haha-service/.../TimedDiscountServiceImpl.java` | 定时折扣管理 |
+| `haha-service/.../InviteActivityServiceImpl.java` | 邀请有礼管理 |
+| `haha-service/.../UnifiedActivityServiceImpl.java` | 统一活动管理 |
+| `haha-service/.../HahaCallbackServiceImpl.java` | 订单回调中优惠券自动扣减 |
+| `haha-admin/.../MarketingActivityController.java` | 运营平台-营销活动API |
+| `haha-admin/.../CouponController.java` | 运营平台-优惠券管理API |
+| `haha-admin/.../TimedDiscountController.java` | 运营平台-定时折扣API |
+| `haha-admin/.../InviteActivityAdminController.java` | 运营平台-邀请有礼API |
+| `haha-admin/.../UnifiedActivityController.java` | 运营平台-统一活动API |
+| `haha-miniapp/.../AppCouponController.java` | 小程序-优惠券API |
+| `haha-miniapp/.../InviteController.java` | 小程序-邀请API |
+| `haha-common/.../enums/ActivityTypeEnum.java` | 活动类型枚举 |
+| `haha-common/.../enums/ActivityStatusEnum.java` | 活动状态枚举 |
+| `haha-common/.../enums/CouponTypeEnum.java` | 优惠券类型枚举 |
+| `haha-common/.../enums/CouponStatusEnum.java` | 优惠券状态枚举 |
+| `haha-common/.../enums/DiscountTypeEnum.java` | 折扣类型枚举 |

+ 275 - 0
docs/订单与支付模块.md

@@ -0,0 +1,275 @@
+# 订单与支付模块业务说明
+
+> 本文档描述订单与支付核心链路,涵盖订单生命周期、支付体系、金额字段定义与退款逻辑。
+> 帮助开发人员快速理解"为什么这样设计"以及各环节的特殊处理方式。
+
+---
+
+## 一、订单核心实体
+
+### 1.1 Order 实体 (`haha-entity/.../Order.java`)
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | Long | 雪花算法主键 |
+| orderNo | String | 设备端订单号(设备回调时写入) |
+| outTradeNo | String | 外部交易号 |
+| activityId | String | 哈哈平台活动ID(用于幂等防重复创建) |
+| userId | Long | 用户ID |
+| deviceId | String | 设备SN号 |
+| totalAmount | BigDecimal | 订单总金额(原价),设备回调时写入 |
+| discountAmount | BigDecimal | 订单优惠金额(营销活动 + 优惠券),默认0 |
+| paidAmount | BigDecimal | 实付总金额 = totalAmount - discountAmount |
+| payStatus | String | 支付状态:UNPAID / PAID / REFUND |
+| payType | String | 支付方式 |
+| payChannel | String | 支付渠道:wechat / alipay / alipay_credit / balance / wechat_payscore |
+| transactionId | String | 第三方交易号 |
+| refundStatus | String | 退款状态 |
+| refundAmount | BigDecimal | 退款金额 |
+| refundTime | LocalDateTime | 退款时间 |
+| refundReason | String | 退款原因 |
+| payScoreOrderId | String | 支付分服务订单号(微信侧) |
+| payScoreState | String | 支付分状态:CREATED / DOING / USER_PAYING / DONE |
+| status | Integer | 订单状态:0-已取消 1-待支付 2-已完成 3-已关闭 |
+| orderType | String | 订单类型:OUT-正常消费,IN-异物放入 |
+| items | String | 商品识别信息JSON |
+| videoUrl | String | 识别视频URL |
+| confidence | BigDecimal | AI识别置信度 |
+
+### 1.2 订单状态枚举 (`OrderStatus`)
+
+| code | 枚举值 | 说明 |
+|------|--------|------|
+| 0 | CANCELLED | 已取消 |
+| 1 | PENDING_PAYMENT | 待支付 |
+| 2 | COMPLETED | 已完成 |
+| 3 | CLOSED | 已关闭 |
+
+### 1.3 支付状态枚举 (`PayStatus`)
+
+| code | 枚举值 | 说明 |
+|------|--------|------|
+| UNPAID | 未支付 | 订单已创建,等待支付 |
+| PAID | 已支付 | 扣费成功 |
+| REFUND | 已退款 | 已完成退款 |
+
+---
+
+## 二、订单完整生命周期
+
+```
+用户扫码开门
+    ↓
+DoorRecord 记录开门(openType=OUT, doorStatus=OPENED)
+    ↓
+用户购物,关门
+    ↓
+设备AI识别回调 handleOrcResult()
+    ↓
+创建订单 createOrderFromRecognition()
+    - totalAmount = 0, discountAmount = 0, paidAmount = 0
+    - status = PENDING_PAYMENT, payStatus = UNPAID
+    ↓
+设备端计算金额(商品原价 - 营销活动优惠)
+    ↓
+订单回调 handleOrderCallback()
+    - 写入 totalAmount(设备传来的已含营销活动优惠的金额)
+    - 初始化 discountAmount = 0, paidAmount = totalAmount
+    ↓
+优惠券自动扣减 processCouponDiscount()
+    - 如有可用优惠券:计算优惠 → 更新 discountAmount / paidAmount
+    ↓
+支付分扣费 / 其他支付渠道
+    - 使用 paidAmount 进行扣费
+    ↓
+支付回调 → 更新 payStatus = PAID, status = COMPLETED
+    ↓
+订单完成 → 触发邀请奖励激活
+```
+
+### 关键说明
+
+1. **AI识别结果不含价格**:`sku_list` 只包含 `{code, quantity}`,商品价格在设备端查询
+2. **订单创建时金额为0**:因AI识别回调不含价格,金额在订单回调时由设备端传入
+3. **营销活动优惠在设备端计算**:设备查询适用活动并计算折扣,传回的是扣减后金额
+4. **优惠券优惠在服务端计算**:`HahaCallbackServiceImpl.processCouponDiscount()` 自动匹配
+
+### 业务背景与设计考量
+
+#### 为什么AI识别结果不含价格?
+
+哈哈平台(设备端SDK)的AI视觉识别只负责识别"哪个商品被拿走了",返回商品编码(code)和数量(quantity),不涉及定价逻辑。商品价格由哈哈平台根据设备绑定的门店信息独立维护,服务端无法在AI识别回调时获取价格。这是设备平台架构决定的——**识别与定价解耦**。
+
+#### 为什么营销活动优惠在设备端计算?
+
+设备端在用户关门后需要**立即展示应付金额**,如果等服务端计算再返回,用户等待时间过长。此外,哈哈平台设备端本身具备查询营销活动和计算优惠的能力。因此采用**设备端计算、服务端信任**的策略:
+- 设备端:查询营销活动 → 计算优惠 → 回调通知服务端
+- 服务端:`MarketingRuleService` 作为备用校验能力,但当前不覆盖主流程
+
+#### 为什么优惠券是自动扣减而非用户手动选择?
+
+智能售卖机的购物场景是**用户关门后自动结算**,没有购物车确认环节,用户无法在结算时手动选择优惠券。因此系统在订单回调时自动为用户匹配最优优惠券(优先使用最先到期的符合条件的券),这是业务场景决定的特殊处理。
+
+#### 为什么订单创建时金额为0?
+
+AI识别回调(`handleOrcResult`)只告知"哪些商品被拿走了",不含价格信息。价格和最终金额要等设备端完成计算后通过订单回调(`handleOrderCallback`)传回。两步回调之间有时间差,期间订单金额为0是正常状态,不是异常。
+
+#### 订单幂等性保证
+
+通过 `activityId`(哈哈平台活动ID)保证订单不重复创建。AI识别回调可能因网络重试多次到达,`createOrderFromRecognition()` 先查询是否已有该 activityId 的订单,有则直接返回,避免重复创建。
+
+---
+
+## 三、金额字段体系
+
+### 3.1 三级金额定义
+
+| 字段 | 含义 | 计算方式 | 赋值时机 |
+|------|------|---------|---------|
+| totalAmount | 订单总金额(原价) | 设备端计算 | 订单回调时由设备传入 |
+| discountAmount | 订单优惠金额 | 营销活动优惠 + 优惠券优惠 | 优惠券扣减时写入 |
+| paidAmount | 实付总金额 | totalAmount - discountAmount | 优惠券扣减后计算 |
+
+#### 为什么需要 discountAmount 和 paidAmount 字段?
+
+**业务背景**:早期订单表只有 `totalAmount`,运营平台和小程序展示“实付金额”时直接取 `totalAmount`。但引入优惠券后,`totalAmount` 存的是设备端传来的金额(已含营销活动优惠但不含优惠券优惠),导致前端显示的“实付”不正确。
+
+**解决方案**:增加 `discountAmount`(记录优惠总额)和 `paidAmount`(记录实付总额),使三级金额语义清晰,前端取值简单可靠。
+
+**特殊处理**:
+- 设备端传来的 `totalAmount` 已包含营销活动优惠(如首单立减5元后传95元),但营销活动优惠金额不单独持久化到订单表
+- `discountAmount` 目前仅记录**服务端计算的优惠券优惠**部分
+- 历史订单迁移:`UPDATE t_order SET discount_amount = 0, paid_amount = total_amount WHERE ...`
+
+### 3.2 金额赋值阶段
+
+| 阶段 | totalAmount | discountAmount | paidAmount |
+|------|-------------|----------------|------------|
+| 创建订单 | 0 | 0 | 0 |
+| 订单回调 | 设备传来 | 0 | = totalAmount |
+| 优惠券扣减 | 不变 | 优惠券金额 | = total - discount |
+| 支付扣费 | 不变 | 不变 | 使用此金额 |
+
+### 3.3 前端兼容性处理
+
+所有前端展示优先使用 `paidAmount`,兼容历史数据:
+
+```typescript
+// 小程序
+const displayAmount = order.paidAmount || order.totalAmount || 0;
+
+// 运营平台
+const amount = row.paidAmount || (row.totalAmount || 0) - (row.discountAmount || 0);
+```
+
+**退款页面**使用 `totalAmount`(基于订单原价计算退款),不使用 `paidAmount`。
+
+#### 为什么退款基于 totalAmount 而非 paidAmount?
+
+退款金额应基于用户实际支付的金额来计算。但当前场景下,`paidAmount` 是服务端计算的优惠券扣减后金额,而设备端可能还有营销活动优惠未体现在 `discountAmount` 中。为避免退款金额计算混乱,退款暂时基于 `totalAmount`(设备端传来的已含营销优惠的金额),后续如需精确退款需结合 `paidAmount` 使用。
+
+---
+
+## 四、支付体系
+
+### 4.1 支付渠道 (`PaymentChannel`)
+
+| code | 枚举值 | 说明 |
+|------|--------|------|
+| wechat | WECHAT | 微信支付(JSAPI) |
+| alipay | ALIPAY | 支付宝支付 |
+| alipay_credit | ALIPAY_CREDIT | 支付宝信用支付(芝麻信用) |
+| balance | BALANCE | 余额支付 |
+| wechat_payscore | WECHAT_PAYSCORE | 微信支付分(先享后付) |
+
+### 4.2 支付架构
+
+```
+PaymentService (门面)
+    ├── PaymentChannelFactory (渠道工厂)
+    ├── WxPayStrategy (微信支付策略)
+    ├── AliPayStrategy (支付宝策略)
+    ├── AliCreditPayStrategy (信用支付策略)
+    └── PayScoreService (支付分服务)
+```
+
+- **策略模式**:`PaymentStrategy` 接口,每种渠道一个实现
+- **门面服务**:`PaymentServiceImpl` 统一调度,上层无需感知渠道差异
+
+### 4.3 微信支付分流程(核心支付方式)
+
+#### 为什么微信支付分是核心支付方式?
+
+智能售卖机场景下,用户**先开门购物,后结算付款**,属于典型的“先享后付”场景。微信支付分天然支持这种模式:
+1. 用户扫码时创建支付分服务订单(不扣款,仅授权)
+2. 用户购物完成后,服务端完结支付分订单(自动扣款)
+3. 无需用户在购物后手动支付,体验最流畅
+
+相比传统支付(用户需主动扫码付款),支付分消除了“忘记付款”的风险,也提升了转化率。
+
+#### 扫码开门的支付分强制校验
+
+为保证扣费安全性,扫码开门流程中会强制检查用户是否已开通微信支付分:
+- 未开通 → 引导用户授权开通 → 开通成功后才能开门
+- 已开通 → 创建支付分服务订单 → 开门
+- 这保证了每个开门用户都具备扣费能力,避免“开门购物但无法扣款”的风险
+
+```
+用户扫码 → 检查支付分开通状态
+    ↓
+未开通 → enablePayscore() → 引导用户授权
+    ↓
+已开通 → createUserPayScoreOrder() → 创建支付分服务订单
+    ↓
+用户开门购物 → 关门 → 订单回调
+    ↓
+completeUserPayScoreOrder() → 完结支付分(自动扣款)
+    ↓
+支付分回调 → 更新订单 payStatus = PAID
+```
+
+**支付分服务订单状态**:
+- CREATED:已创建
+- DOING:服务中(用户正在购物)
+- USER_PAYING:用户支付中
+- DONE:已完成(扣款成功)
+
+### 4.4 退款流程
+
+```
+运营平台发起退款
+    ↓
+OrderServiceImpl.refund()
+    - 验证订单状态和支付状态
+    - 调用 PaymentService.refund()
+    - 更新 refundStatus / refundAmount / refundTime
+```
+
+#### 退款特殊处理
+
+- **支付分退款**:支付分订单完结后走微信退款接口,需传入原始支付分订单号
+- **优惠券不可退回**:订单退款时,已使用的优惠券状态不会回滚为“未使用”,这是当前的业务规则,如需退回需额外开发
+- **部分退款**:运营人员可输入退款金额,部分退款是允许的(如仅退某个商品)
+- **异物放入订单**(orderType=IN)不展示给用户端,退款需在运营平台操作
+
+---
+
+## 五、核心代码文件索引
+
+| 文件 | 职责 |
+|------|------|
+| `haha-entity/.../Order.java` | 订单实体 |
+| `haha-entity/.../OrderGoods.java` | 订单商品明细 |
+| `haha-service/.../OrderServiceImpl.java` | 订单服务(创建、查询、退款) |
+| `haha-service/.../HahaCallbackServiceImpl.java` | 设备回调处理(订单回调、优惠券扣减) |
+| `haha-service/.../payment/PaymentService.java` | 支付门面接口 |
+| `haha-service/.../payment/impl/PaymentServiceImpl.java` | 支付门面实现 |
+| `haha-service/.../payment/payscore/PayScoreService.java` | 支付分服务接口 |
+| `haha-admin/.../OrderController.java` | 运营平台订单API |
+| `haha-miniapp/.../OrderController.java` | 小程序订单API |
+| `haha-miniapp/.../PaymentController.java` | 小程序支付API |
+| `haha-miniapp/.../PayScoreController.java` | 小程序支付分API |
+| `haha-common/.../constant/OrderConstants.java` | 订单常量定义 |
+| `haha-common/.../enums/OrderStatus.java` | 订单状态枚举 |
+| `haha-common/.../enums/PayStatus.java` | 支付状态枚举 |
+| `haha-common/.../enums/PaymentChannel.java` | 支付渠道枚举 |

+ 322 - 0
docs/设备与门店模块.md

@@ -0,0 +1,322 @@
+# 设备与门店模块业务说明
+
+> 本文档描述设备管理、门店管理、库存管理及设备回调链路。
+> 帮助开发人员快速理解"为什么这样设计"以及各环节的特殊处理方式。
+
+---
+
+## 一、设备管理
+
+### 1.1 Device 实体
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | Long | 雪花算法主键 |
+| deviceId | String | 设备SN号(唯一标识) |
+| shopId | Long | 所属门店ID |
+| name | String | 设备名称 |
+| authToken | String | 设备认证Token |
+| status | Integer | 设备状态 |
+| doorStatus | String | 门状态:ERROR / OPENED / CLOSED / ANOTHER(设备繁忙) |
+| currentInventoryHash | String | 当前库存哈希(用于同步判断) |
+| createTime / updateTime | LocalDateTime | 时间戳 |
+
+**非DB字段**(`@TableField(exist = false)`):shopName, address, deviceTypeLabel, statusLabel, statusColor, isOnline, temperature, volume, doorCount, layerCount, hasScreen, totalSales, todaySales, orderCount, lastOnlineTime
+
+### 1.2 门状态枚举 (`DeviceDoorStatus`)
+
+| code | 枚举值 | 说明 |
+|------|--------|------|
+| ERROR | 开门失败 | 远程开门请求失败 |
+| OPENED | 门已开 | 门处于打开状态 |
+| CLOSED | 门已关 | 门处于关闭状态 |
+| ANOTHER | 设备繁忙 | 设备正忙,无法开门 |
+
+#### 为什么门状态用字符串而非数字编码?
+
+门状态直接来自哈哈平台设备端的回调,设备端使用字符串状态码(如 "1"=ERROR, "2"=OPENED, "3"=CLOSED, "4"=ANOTHER)。为避免数字编码与数据库值之间的映射混乱,`DeviceDoorStatus` 枚举在数据库中存储英文状态字符串(ERROR/OPENED/CLOSED/ANOTHER),并在 `convertToDbValue()` 方法中处理原始数字码到字符串的转换。这种设计使得数据库中的值自解释,排查问题时一目了然。
+
+### 1.3 设备服务核心方法 (`DeviceService`)
+
+| 方法 | 说明 |
+|------|------|
+| getPage() | 分页查询设备列表(支持门店/状态筛选) |
+| getDetailById() | 设备详情(含门店名、状态标签填充) |
+| getStatistics() | 设备统计数据 |
+| openDoor() | 远程开门 |
+| scanCodeOpenDoor() | 扫码开门业务(含支付分校验) |
+| setTemperature() | 设置温度 |
+| setVolume() | 设置音量 |
+| getNearbyDevices() | 附近设备查询(经纬度+距离) |
+| getDeviceBySn() | 通过SN号查询设备 |
+| updateDeviceStatus() | 更新设备状态 |
+| updateInventoryHash() | 更新库存哈希 |
+
+---
+
+## 二、设备回调处理 (`HahaCallbackService`)
+
+#### 什么是设备回调?
+
+哈哈平台(智能售卖机设备平台)采用回调机制主动推送设备事件。我们的服务端作为回调接收方,在 `CallbackController` 中接收并分发处理。回调类型包括设备状态变更、AI识别结果、订单信息等。
+
+**为什么用回调而非轮询?**
+- 实时性:设备事件立即推送,无需等待轮询周期
+- 高效:只有事件发生时才产生网络请求,节省资源
+- 可靠:回调失败可重试,配合签名验证保证安全
+
+### 2.1 回调消息类型
+
+| 类型 | 方法 | 说明 |
+|------|------|------|
+| DEVICE_STATUS | handleDeviceStatus() | 开关门状态通知 |
+| ONLINE_STATUS | handleOnlineStatus() | 设备在线/离线通知 |
+| VOICE_RESULT | handleVoiceResult() | 音量调节结果 |
+| CLIENT_NEW_PRODUCT | handleNewProductAudit() | 新品审核结果 |
+| MERGE_PRODUCT | handleMergeProduct() | 商品合并结果 |
+| ORC_RESULT | handleOrcResult() | AI识别结果通知 |
+| 订单回调 | handleOrderCallback() | 设备端订单信息回调 |
+
+### 2.2 核心回调流程
+
+#### ORC_RESULT(AI识别回调)
+
+```
+设备AI识别完成
+    ↓
+解析 sku_list: [{code, quantity}]
+    ↓
+创建/查找订单 createOrderFromRecognition()
+    - 幂等:通过 activityId 防重复
+    - 订单金额初始化为0(等待设备端回调)
+    ↓
+解析视频URL等资源信息
+    ↓
+创建 DoorRecord(doorStatus=CLOSED)
+```
+
+#### 订单回调
+
+```
+设备端计算好订单金额和商品明细后回调
+    ↓
+查找或创建本地订单
+    ↓
+updateOrderFromCallback()
+    - 写入 orderNo, totalAmount, items
+    - 初始化 discountAmount=0, paidAmount=totalAmount
+    ↓
+saveOrderGoods() — 保存订单商品明细
+    ↓
+processCouponDiscount() — 自动优惠券扣减
+    ↓
+processPayScorePayment() — 支付分扣费
+```
+
+---
+
+## 三、开关门记录 (DoorRecord)
+
+### 3.1 实体字段
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | Long | 雪花算法主键 |
+| activityId | String | 哈哈平台活动ID |
+| deviceId | String | 设备ID |
+| userId | Long | 用户ID |
+| doorIndex | String | 门索引(A/B/C/D) |
+| openType | String | 开门类型:IN-上货,OUT-消费 |
+| doorStatus | String | 门状态:OPENED-已开,CLOSED-已关 |
+| nobuy | Integer | 是否无消费:0-有消费,1-无消费 |
+| orderId | Long | 关联订单ID(有消费时) |
+| openTime | LocalDateTime | 开门时间 |
+| closeTime | LocalDateTime | 关门时间 |
+| duration | Integer | 持续时长(秒) |
+| source | String | 来源:MINIAPP / ADMIN |
+| remark | String | 备注 |
+
+### 3.2 开门类型 (`OpenType`)
+
+| code | 说明 |
+|------|------|
+| IN | 上货开门(运营人员补货) |
+| OUT | 消费开门(用户购物) |
+
+---
+
+## 四、门店管理
+
+### 4.1 Shop 实体
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | Long | 雪花算法主键 |
+| shopCode | String | 门店编码(唯一标识) |
+| name | String | 门店名称 |
+| address | String | 门店地址 |
+| province / city / district | String | 省市区 |
+| longitude / latitude | Double | 经纬度 |
+| contactName / contactPhone | String | 联系人/电话 |
+| businessHours | String | 营业时间 |
+| status | Integer | 状态:0-禁用 1-启用 |
+| remark | String | 备注 |
+
+**非DB字段**:deviceCount(设备总数), onlineCount(在线设备数), statusLabel, statusColor
+
+### 4.2 门店服务核心方法 (`ShopService`)
+
+| 方法 | 说明 |
+|------|------|
+| getPage() | 分页查询门店 |
+| getDetailById() | 门店详情(含设备数/在线数) |
+| getStatistics() | 门店统计 |
+| getNearbyShops() | 附近门店查询(小程序端) |
+
+### 4.3 门店补货员 (ShopReplenisher)
+
+关联门店和补货人员,用于补货权限管理。
+
+---
+
+## 五、库存管理
+
+### 5.1 DeviceInventory 实体(设备商品库存)
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | Long | 雪花算法主键 |
+| deviceId | String | 设备ID(SN号) |
+| productId | Long | 商品ID(本地) |
+| productCode | String | 商品编码(哈哈平台code) |
+| productName | String | 商品名称(冗余) |
+| stock | Integer | 当前库存数量 |
+| shelfNum | Integer | 货架层号 |
+| position | String | 货道位置(left/right) |
+| warningThreshold | Integer | 库存预警阈值 |
+| lastRestockTime | LocalDateTime | 最后补货时间 |
+
+### 5.2 库存服务 (`DeviceInventoryService`)
+
+| 方法 | 说明 |
+|------|------|
+| getInventoryByDevice() | 查询设备库存列表 |
+| syncInventory() | 同步设备库存(从设备端获取最新数据) |
+| deductStock() | 扣减库存(订单完成后) |
+| checkLowStock() | 低库存预警检查 |
+
+### 5.3 库存同步流程
+
+```
+设备端上报库存变更
+    ↓
+HahaCallbackService 接收通知
+    ↓
+比对 currentInventoryHash
+    ↓
+如不一致 → 调用哈哈SDK获取最新库存
+    ↓
+更新 DeviceInventory 记录
+    ↓
+更新 currentInventoryHash
+    ↓
+检查低库存 → 触发告警
+```
+
+#### 库存哈希机制的设计考量
+
+`currentInventoryHash` 是设备当前库存的哈希摘要。每次设备端回调库存变更时,先比对哈希值:
+- 哈希一致 → 库存未变化,跳过同步,减少不必要的SDK调用
+- 哈希不一致 → 库存有变化,拉取最新数据并更新
+
+这种设计避免了频繁全量同步的性能开销,只在库存确实变化时才触发同步。
+
+### 5.4 补货记录 (StockRecord / StockRecordItem)
+
+| 实体 | 说明 |
+|------|------|
+| StockRecord | 补货记录主表(操作人、设备、时间) |
+| StockRecordItem | 补货明细(商品、数量变化) |
+
+---
+
+## 六、设备告警体系
+
+### 6.1 DeviceAlertRecord 实体
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | Long | 雪花算法主键 |
+| deviceId | String | 设备ID |
+| alertType | String | 告警类型 |
+| alertLevel | Integer | 告警级别 |
+| title | String | 告警标题 |
+| content | String | 告警内容 |
+| status | Integer | 处理状态 |
+
+### 6.2 告警类型 (`DeviceAlertType`)
+
+| code | 说明 |
+|------|------|
+| OFFLINE | 设备离线 |
+| LOW_STOCK | 低库存 |
+| DOOR_ERROR | 开门异常 |
+
+### 6.3 告警通知
+
+告警通过企业微信 Webhook 推送,配置在 `haha-service/.../notify/` 中。
+
+---
+
+## 七、商品管理
+
+### 7.1 Product 实体
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| id | Long | 雪花算法主键 |
+| productId | String | 哈哈平台商品ID |
+| code | String | 商品编码(哈哈平台code) |
+| barcode | String | 商品条码 |
+| name | String | 商品名称 |
+| type | String | 商品分类 |
+| pic | String | 商品图片URL |
+| retailPrice | Double | 零售价 |
+| costPrice | Double | 成本价 |
+| applyStatus | String | 适用设备:全部柜/静态柜/动态柜 |
+| syncStatus | Integer | 同步状态:0-未同步 1-同步中 2-已同步 3-同步失败 |
+
+### 7.2 商品同步
+
+商品数据从哈哈平台(SDK)同步到本地,`DataSyncServiceImpl` 处理全量/增量同步。
+
+---
+
+## 八、核心代码文件索引
+
+| 文件 | 职责 |
+|------|------|
+| `haha-entity/.../Device.java` | 设备实体 |
+| `haha-entity/.../Shop.java` | 门店实体 |
+| `haha-entity/.../Product.java` | 商品实体 |
+| `haha-entity/.../DoorRecord.java` | 开关门记录 |
+| `haha-entity/.../DeviceInventory.java` | 设备库存 |
+| `haha-entity/.../StockRecord.java` | 补货记录 |
+| `haha-entity/.../DeviceAlertRecord.java` | 设备告警记录 |
+| `haha-service/.../DeviceServiceImpl.java` | 设备服务 |
+| `haha-service/.../ShopServiceImpl.java` | 门店服务 |
+| `haha-service/.../ProductServiceImpl.java` | 商品服务 |
+| `haha-service/.../DoorRecordServiceImpl.java` | 开关门记录服务 |
+| `haha-service/.../DeviceInventoryServiceImpl.java` | 库存服务 |
+| `haha-service/.../DeviceAlertServiceImpl.java` | 告警服务 |
+| `haha-service/.../HahaCallbackServiceImpl.java` | 设备回调处理 |
+| `haha-service/.../DataSyncServiceImpl.java` | 数据同步服务 |
+| `haha-admin/.../DeviceController.java` | 运营平台-设备API |
+| `haha-admin/.../ShopController.java` | 运营平台-门店API |
+| `haha-admin/.../InventoryController.java` | 运营平台-库存API |
+| `haha-admin/.../DeviceAlertController.java` | 运营平台-告警API |
+| `haha-miniapp/.../DeviceController.java` | 小程序-设备API |
+| `haha-common/.../enums/DeviceDoorStatus.java` | 门状态枚举 |
+| `haha-common/.../enums/DeviceAlertType.java` | 告警类型枚举 |
+| `haha-common/.../enums/DeviceOnlineStatus.java` | 在线状态枚举 |
+| `haha-common/.../constant/DeviceConstants.java` | 设备常量 |

+ 475 - 0
haha-admin-web/AGENTS.md

@@ -0,0 +1,475 @@
+# AGENTS.md - haha-admin-web 智能体代码生成规则
+
+> 本文件为 AI Agent 代码生成提供强制规则约束,定义生成边界与规范。
+> 所有规则均为强制性,Agent 生成代码时必须严格遵守。违反规则的代码不可提交。
+
+---
+
+## 1. 项目边界
+
+### 1.1 技术栈版本锁定
+
+| 技术 | 版本 | 禁止事项 |
+|------|------|----------|
+| Node.js | ^20.19.0 \|\| >=22.13.0 | 禁止使用 Node 18 及以下 |
+| pnpm | >=9 | 禁止使用 npm/yarn |
+| Vue | ^3.5.25 | Composition API only,禁止 Options API |
+| TypeScript | ^5.9.3 | 禁止 any 滥用,新代码必须提供类型 |
+| Vite | ^7.2.7 | -- |
+| Element Plus | ^2.12.0 | 禁止引入 Ant Design / Naive UI 等其他组件库 |
+| Pinia | ^3.0.4 | 禁止 Vuex |
+| Vue Router | ^4.6.3 | -- |
+| Tailwind CSS | ^4.1.17 | 用于工具类,禁止替代组件级样式 |
+| ECharts | ^6.0.0 | 图表统一使用 ECharts |
+| Axios | ^1.13.2 | HTTP 请求统一使用 `http` 实例,禁止直接 `axios` |
+| dayjs | ^1.11.19 | 禁止引入 moment.js |
+| Sass | ^1.95.1 | 样式预处理统一使用 SCSS |
+
+### 1.2 后端 API 对接
+
+| 配置项 | 值 |
+|--------|-----|
+| 后端服务地址 | `http://localhost:7070/admin` |
+| 开发端口 | 8888 |
+| API 代理 | vite.config.ts 中 `apiPaths` 动态生成 |
+| 生产 publicPath | `/admin/` |
+| 路由模式 | hash |
+
+### 1.3 目录职责(严格遵循)
+
+```
+src/
+├── api/          # API 请求函数(每个模块一个文件)
+├── assets/       # 静态资源(图标、图片、字体)
+├── components/   # 全局公共组件(ReXxx 命名)
+├── config/       # 应用配置
+├── directives/   # 自定义指令
+├── layout/       # 布局框架
+├── plugins/      # 插件注册(ElementPlus、I18n、ECharts、VxeTable)
+├── router/       # 路由配置(modules/ 下按模块拆分)
+├── store/        # Pinia 状态管理(modules/ 下按模块拆分)
+├── style/        # 全局样式(reset、index、tailwind、element-plus、dark)
+├── utils/        # 工具函数
+├── views/        # 页面视图(每个模块一个目录)
+│   └── {module}/
+│       ├── index.vue      # 页面入口
+│       └── utils/
+│           ├── hook.tsx   # 业务逻辑 hook(composable)
+│           └── types.ts   # 模块类型定义
+└── main.ts       # 应用入口
+```
+
+---
+
+## 2. 代码生成规则
+
+### 2.1 页面视图 (views)
+
+**强制结构**:每个业务页面必须按以下模式组织:
+
+```
+views/{module}/
+├── index.vue       # 纯模板 + 结构,不含业务逻辑
+└── utils/
+    ├── hook.tsx    # 业务逻辑 composable(export function useXxx)
+    └── types.ts    # 模块内类型定义
+```
+
+**index.vue 模板**:
+```vue
+<script setup lang="ts">
+import { ref } from "vue";
+import { useXxx } from "./utils/hook";  // 使用 hook.tsx 中的 composable
+import { PureTableBar } from "@/components/RePureTableBar";
+import { useRenderIcon } from "@/components/ReIcon/src/hooks";
+
+defineOptions({
+  name: "XxxManage"  // 必须:组件名使用 PascalCase + Manage
+});
+
+const formRef = ref();
+const tableRef = ref();
+
+const {
+  form, loading, columns, dataList, pagination,
+  onSearch, resetForm, handleSizeChange, handleCurrentChange,
+  // ...其他业务方法
+} = useXxx(tableRef);
+</script>
+
+<template>
+  <div class="main">
+    <!-- 搜索表单 -->
+    <el-form ref="formRef" :inline="true" :model="form"
+             class="search-form bg-bg_color w-full pl-8 pt-[12px] overflow-auto">
+      <!-- el-form-item 列表 -->
+    </el-form>
+
+    <!-- 表格 -->
+    <PureTableBar title="xxx管理" :columns="columns" @refresh="onSearch">
+      <template v-slot="{ size, dynamicColumns }">
+        <pure-table
+          ref="tableRef"
+          row-key="id"
+          adaptive
+          :adaptiveConfig="{ offsetBottom: 108 }"
+          align-whole="center"
+          table-layout="auto"
+          :loading="loading"
+          :size="size"
+          :data="dataList"
+          :columns="dynamicColumns"
+          :pagination="{ ...pagination, size }"
+          :header-cell-style="{
+            background: 'var(--el-fill-color-light)',
+            color: 'var(--el-text-color-primary)'
+          }"
+          @page-size-change="handleSizeChange"
+          @page-current-change="handleCurrentChange"
+        >
+          <template #operation="{ row }">
+            <!-- 操作按钮 -->
+          </template>
+        </pure-table>
+      </template>
+    </PureTableBar>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.search-form {
+  :deep(.el-form-item) {
+    margin-bottom: 12px;
+  }
+}
+</style>
+```
+
+**hook.tsx 模板**:
+```tsx
+import { ref, reactive, onMounted } from "vue";
+import type { PaginationProps } from "@pureadmin/table";
+import { message } from "@/utils/message";
+import { getXxxList } from "@/api/xxx";
+import type { XxxItem, XxxSearchForm } from "./types";
+
+export function useXxx(tableRef: Ref) {
+  const form = reactive<XxxSearchForm>({ /* 初始值 */ });
+  const dataList = ref([]);
+  const loading = ref(true);
+  const pagination = reactive<PaginationProps>({
+    total: 0,
+    pageSize: 10,
+    currentPage: 1,
+    background: true
+  });
+
+  const columns: TableColumnList = [
+    { label: "xxx", prop: "xxx", minWidth: 120 },
+    // ...
+  ];
+
+  const onSearch = async () => {
+    loading.value = true;
+    try {
+      const { data } = await getXxxList({ ...form, page: pagination.currentPage, pageSize: pagination.pageSize });
+      dataList.value = data.list;
+      pagination.total = data.total;
+    } finally {
+      loading.value = false;
+    }
+  };
+
+  const resetForm = (formEl: any) => {
+    if (!formEl) return;
+    formEl.resetFields();
+    onSearch();
+  };
+
+  const handleSizeChange = (val: number) => {
+    pagination.pageSize = val;
+    onSearch();
+  };
+
+  const handleCurrentChange = (val: number) => {
+    pagination.currentPage = val;
+    onSearch();
+  };
+
+  onMounted(() => { onSearch(); });
+
+  return {
+    form, loading, columns, dataList, pagination,
+    onSearch, resetForm, handleSizeChange, handleCurrentChange
+  };
+}
+```
+
+**types.ts 模板**:
+```ts
+export interface XxxItem {
+  id: number;
+  // 字段定义
+}
+
+export interface XxxSearchForm {
+  // 搜索表单字段
+}
+```
+
+### 2.2 API 层 (api/)
+
+**强制规则**:
+
+1. 每个 API 文件对应一个后端模块,文件名与后端路径一致
+2. 统一使用 `http.request<T>(method, url, config)` 调用
+3. 必须定义 `Result` 和 `ResultTable` 类型(可复用已有定义)
+
+**API 文件模板**:
+```ts
+import { http } from "@/utils/http";
+
+type Result = {
+  code: number;
+  message: string;
+  data?: any;
+};
+
+type ResultTable = {
+  code: number;
+  message: string;
+  data?: {
+    list: Array<any>;
+    total: number;
+    pageSize: number;
+    currentPage: number;
+  };
+};
+
+// 列表查询
+export const getXxxList = (params: {
+  page?: number;
+  pageSize?: number;
+  // ...搜索参数
+}) => {
+  return http.request<ResultTable>("get", "/xxx/list", { params });
+};
+
+// 详情查询
+export const getXxxById = (id: number) => {
+  return http.request<Result>("get", `/xxx/${id}`);
+};
+
+// 新增
+export const createXxx = (data: any) => {
+  return http.request<Result>("post", "/xxx", { data });
+};
+
+// 修改状态
+export const updateXxxStatus = (id: number, status: number) => {
+  return http.request<Result>("put", `/xxx/${id}/status?status=${status}`);
+};
+
+// 删除
+export const deleteXxx = (id: number) => {
+  return http.request<Result>("delete", `/xxx/${id}`);
+};
+```
+
+**禁止事项**:
+- 禁止直接 `import axios` 调用,必须使用 `http` 实例
+- 禁止在 API 函数中处理 UI 逻辑(如 ElMessage)
+- 禁止硬编码后端地址
+
+### 2.3 路由 (router/modules/)
+
+**路由文件模板**:
+```ts
+export default {
+  path: "/xxx",
+  redirect: "/xxx/list",
+  meta: {
+    icon: "ri:xxx-line",       // Remix Icon
+    title: "xxx管理",
+    rank: 数字                  // 菜单排序
+  },
+  children: [
+    {
+      path: "/xxx/list",
+      name: "XxxList",         // PascalCase
+      component: () => import("@/views/xxx/index.vue"),
+      meta: {
+        icon: "ri:xxx-line",
+        title: "xxx管理"
+      }
+    }
+  ]
+} satisfies RouteConfigsTable;
+```
+
+**强制规则**:
+- 路由 path 使用 kebab-case
+- 路由 name 使用 PascalCase
+- 图标统一使用 Remix Icon(`ri:xxx-line`)
+- 懒加载必须用 `() => import()` 语法
+
+### 2.4 Store (store/modules/)
+
+**Store 模板**:
+```ts
+import { defineStore } from "pinia";
+import { store } from "../utils";
+
+export const useXxxStore = defineStore("pure-xxx", {
+  state: (): XxxState => ({
+    // 状态
+  }),
+  actions: {
+    // 方法命名:SET_XXX (修改状态)、async方法 (异步操作)
+  }
+});
+
+export function useXxxStoreHook() {
+  return useXxxStore(store);
+}
+```
+
+**强制规则**:
+- Store ID 前缀 `pure-`
+- 必须导出 `useXxxStoreHook` 辅助函数
+- 状态修改方法以 `SET_` 前缀命名
+
+### 2.5 组件 (components/)
+
+**强制规则**:
+- 全局公共组件以 `Re` 前缀命名(如 `ReDialog`、`ReIcon`)
+- 组件导出使用 `index.ts` 桶文件
+- 禁止在公共组件中写入业务逻辑
+
+---
+
+## 3. 样式规则
+
+### 3.1 样式方案
+
+| 场景 | 方案 | 说明 |
+|------|------|------|
+| 布局与间距 | Tailwind CSS | `w-full`、`pl-8`、`pt-[12px]` 等 |
+| 组件级样式 | `<style lang="scss" scoped>` | 必须加 scoped |
+| 全局样式 | `src/style/` | reset、主题、动画 |
+| 深度选择器 | `:deep()` | 禁止 `::v-deep`、`/deep/`、`>>>` |
+
+### 3.2 禁止事项
+
+- 禁止在组件内写非 scoped 的全局样式
+- 禁止使用内联 `style=""` 属性(Tailwind 例外)
+- 禁止覆盖 Element Plus 主题变量(通过 CSS 变量定制)
+
+---
+
+## 4. 认证与权限规则
+
+### 4.1 Token 管理
+
+- Token 存储在 Cookie(`authorized-token`)+ localStorage(`user-info`)
+- 请求头格式:`Authorization: Bearer {token}`
+- Token 自动刷新:`PureHttp` 类内置无感刷新机制
+- 401 响应自动登出:响应拦截器处理
+
+### 4.2 权限控制
+
+| 层级 | 实现方式 | 示例 |
+|------|----------|------|
+| 路由级别 | `meta.roles` | 路由守卫校验 |
+| 按钮级别 | `<Auth>` 组件 / `v-perms` 指令 | `<Auth value="device:open">按钮</Auth>` |
+| API 级别 | 后端 `@RequirePermission` | 后端控制 |
+
+### 4.3 登录流程
+
+1. 调用 `login()` API
+2. `setToken()` 存储 token 到 Cookie + localStorage
+3. `useUserStoreHook().SET_ROLES/SET_PERMS` 存储权限
+4. 路由守卫 `initRouter()` 动态加载菜单
+
+---
+
+## 5. 请求与响应规范
+
+### 5.1 响应格式
+
+后端统一返回格式:
+```ts
+type Result = {
+  code: number;     // 200=成功
+  message: string;
+  data?: any;
+};
+
+type ResultTable = {
+  code: number;
+  message: string;
+  data?: {
+    list: Array<any>;
+    total: number;
+    pageSize: number;
+    currentPage: number;
+  };
+};
+```
+
+### 5.2 HTTP 客户端规则
+
+- 统一使用 `src/utils/http/` 中的 `http` 实例
+- 请求超时:10000ms
+- Content-Type:`application/json`
+- 参数序列化:qs (stringify)
+- 禁止在业务代码中直接 `import axios`
+
+---
+
+## 6. 开发规范
+
+### 6.1 命名规则
+
+| 类型 | 命名风格 | 示例 |
+|------|----------|------|
+| 文件名 | kebab-case | `device-manage.vue`、`order.ts` |
+| 组件名 | PascalCase | `DeviceManage`、`PureTableBar` |
+| 变量/函数 | camelCase | `handleOpenDoor`、`dataList` |
+| 常量 | UPPER_SNAKE_CASE | `VITE_PORT` |
+| CSS 类名 | kebab-case / BEM | `search-form`、`order-item` |
+| 路由 path | kebab-case | `/device/list` |
+| Store ID | kebab-case + `pure-` 前缀 | `pure-user` |
+| API 函数 | camelCase + 动词前缀 | `getDeviceList`、`createCoupon` |
+
+### 6.2 Git 规范
+
+- Commit 格式遵循 `@commitlint/config-conventional`
+- 允许的类型:`feat`、`fix`、`docs`、`style`、`refactor`、`test`、`chore`
+- 代码提交前自动 lint-staged
+
+### 6.3 代码格式化
+
+- Prettier 配置:双引号、无尾逗号、箭头函数省略括号
+- ESLint:TypeScript strict mode + Vue 3 推荐
+- 禁止 `debugger`(已配置 `no-debugger: off` 但不推荐)
+
+---
+
+## 7. 常见陷阱
+
+| 编号 | 陷阱 | 规则 |
+|------|------|------|
+| T1 | 直接使用 `axios` | 禁止。必须使用 `http` 实例 |
+| T2 | 在 API 层处理 UI 逻辑 | 禁止。API 层只负责请求,UI 反馈在 hook 或组件中处理 |
+| T3 | 组件内写非 scoped 样式 | 禁止。必须 `<style lang="scss" scoped>` |
+| T4 | 不提供 TypeScript 类型 | 禁止。新代码必须提供类型定义 |
+| T5 | 在 index.vue 中写业务逻辑 | 禁止。逻辑必须抽离到 `utils/hook.tsx` |
+| T6 | 路由不使用懒加载 | 禁止。必须 `() => import()` |
+| T7 | 使用 Vuex | 禁止。统一使用 Pinia |
+| T8 | 使用 moment.js | 禁止。统一使用 dayjs |
+| T9 | 修改全局样式时不走 style/ | 禁止。全局样式改动必须修改 `src/style/` 下文件 |
+| T10 | 分页不使用 `PaginationProps` | 禁止。分页配置必须使用 `@pureadmin/table` 的 `PaginationProps` |
+| T11 | 表格不使用 `PureTableBar` | 禁止。列表页必须使用 `PureTableBar` 包裹 |
+| T12 | 图标不使用 Remix Icon | 禁止。统一使用 `ri:` 前缀图标或 `~icons/` 前缀自动导入 |
+| T13 | 表单重置不调用 `resetFields` | 禁止。重置必须使用 Element Plus 的 `resetFields()` |

+ 459 - 0
haha-mp/AGENTS.md

@@ -0,0 +1,459 @@
+# AGENTS.md - haha-mp 智能体代码生成规则
+
+> 本文件为 AI Agent 代码生成提供强制规则约束,定义生成边界与规范。
+> 所有规则均为强制性,Agent 生成代码时必须严格遵守。违反规则的代码不可提交。
+
+---
+
+## 1. 项目边界
+
+### 1.1 技术栈版本锁定
+
+| 技术 | 版本 | 禁止事项 |
+|------|------|----------|
+| uni-app | 3.0.0-5000720260410001 | 禁止使用 uni-app v2 |
+| Vue | 3.5.32 | Composition API only,禁止 Options API |
+| TypeScript | ^4.9.4 | 注意:uni-app 限制,不可升级到 TS 5 |
+| Vite | 5.2.8 | 禁止升级到 Vite 6+(uni-app 兼容性) |
+| Pinia | ^3.0.4 | 禁止 Vuex |
+| Sass | ^1.99.0 | 样式预处理统一使用 SCSS |
+| 微信小程序 appid | `wxef6ffc2591d04b1b` | 禁止修改 |
+
+### 1.2 目标平台
+
+- **主要平台**:微信小程序(`mp-weixin`)
+- 构建命令:`pnpm dev:mp-weixin`(开发)、`pnpm build:mp-weixin`(构建)
+- 代码需兼容小程序运行时限制(无 DOM、无 window、无 axios)
+
+### 1.3 后端 API 对接
+
+| 配置项 | 值 |
+|--------|-----|
+| 后端服务地址 | `https://dev-haha.kuaiyuman.cn/api`(开发环境) |
+| 后端应用 | `haha-miniapp`(端口 7077,context-path: /api) |
+| 请求方式 | `uni.request`(封装在 `src/utils/request.ts`) |
+| 认证方式 | 请求头 `accessToken: {token}` |
+| 请求超时 | 30000ms |
+
+### 1.4 目录职责(严格遵循)
+
+```
+src/
+├── api/          # API 请求函数(每个模块一个文件)
+├── components/   # 公共组件
+├── pages/        # 小程序页面(每个页面一个目录,含 .vue 文件)
+├── static/       # 静态资源(图标、图片、动画样式)
+│   ├── icons/    # SVG 图标
+│   └── images/   # 图片资源
+├── styles/       # 全局样式
+├── utils/        # 工具函数
+│   ├── request.ts  # HTTP 请求封装
+│   ├── auth.ts     # 认证工具
+│   └── config.ts   # API 配置
+├── App.vue       # 应用入口(onLaunch/onShow/onHide)
+├── main.ts       # 创建应用(createSSRApp + Pinia)
+├── manifest.json # 小程序配置
+├── pages.json    # 页面路由与导航栏配置
+└── uni.scss      # 全局 SCSS 变量(Design Tokens)
+```
+
+---
+
+## 2. 代码生成规则
+
+### 2.1 页面 (pages/)
+
+**强制结构**:每个页面是一个独立目录,包含同名的 `.vue` 文件
+
+```
+pages/{pageName}/
+└── {pageName}.vue    # 页面组件
+```
+
+**页面 Vue 文件模板**:
+```vue
+<template>
+  <view class="container">
+    <!-- 页面内容 -->
+  </view>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue';
+import { onShow, onLoad } from '@dcloudio/uni-app';
+import { xxxApi } from '@/api/xxx';
+
+// 响应式数据
+const dataList = ref<XxxType[]>([]);
+const loading = ref(false);
+
+// 页面加载
+onLoad((options: any) => {
+  // 处理页面参数
+});
+
+// 页面显示(每次从后台切回都触发)
+onShow(() => {
+  // 刷新数据
+});
+
+// 方法
+const loadData = async () => {
+  if (loading.value) return;
+  loading.value = true;
+  try {
+    uni.showLoading({ title: '加载中...', mask: true });
+    const result = await xxxApi();
+    dataList.value = result;
+  } catch (error: any) {
+    console.error('加载失败:', error);
+    // 错误已在 request.ts 中统一 toast 提示
+  } finally {
+    uni.hideLoading();
+    loading.value = false;
+  }
+};
+</script>
+
+<style lang="scss">
+/* 使用 Design Token 变量,禁止硬编码颜色/间距 */
+.container {
+  min-height: 100vh;
+  background: $color-bg-secondary;
+  padding: $spacing-lg;
+  box-sizing: border-box;
+}
+</style>
+```
+
+**页面注册**:新增页面必须在 `pages.json` 中注册
+
+```json
+{
+  "path": "pages/xxx/xxx",
+  "style": {
+    "navigationBarTitleText": "页面标题",
+    "navigationBarBackgroundColor": "#FFD700",
+    "navigationBarTextStyle": "black"
+  }
+}
+```
+
+**强制规则**:
+- 导航栏背景色统一 `#FFD700`(品牌黄)
+- 导航栏文字颜色统一 `black`
+- 使用 `<view>` 代替 `<div>`,`<text>` 代替 `<span>`
+- 使用 `<image>` 代替 `<img>`,必须指定 `mode` 属性
+- 使用 `uni.xxx` API 代替浏览器原生 API
+- 使用 `rpx` 单位代替 `px`(1rpx = 屏幕宽度/750)
+- 使用 `@dcloudio/uni-app` 生命周期钩子(`onLoad`、`onShow`、`onHide`)
+- 禁止使用 DOM API(`document`、`window`、`localStorage`)
+- 使用 `uni.getStorageSync/setStorageSync` 代替 `localStorage`
+- 路由跳转使用 `uni.navigateTo`/`uni.reLaunch`/`uni.switchTab`
+
+### 2.2 API 层 (api/)
+
+**强制规则**:
+1. 每个 API 文件对应一个后端模块
+2. 统一使用 `src/utils/request.ts` 中的 `request`/`get`/`post`/`put`/`del`
+3. 必须定义 TypeScript 接口,导出供页面使用
+
+**API 文件模板**:
+```ts
+/**
+ * xxx相关API
+ */
+
+import { get, post } from '../utils/request';
+
+/**
+ * 数据接口定义
+ */
+export interface XxxInfo {
+  id: string;              // 雪花ID,必须为 string 类型
+  name: string;
+  status: number;
+  createTime: string;
+}
+
+/**
+ * 请求参数接口
+ */
+export interface XxxListRequest {
+  page?: number;
+  pageSize?: number;
+  status?: number;
+}
+
+/**
+ * 获取xxx列表
+ * @param params 查询参数
+ */
+export const getXxxList = (params?: XxxListRequest): Promise<XxxInfo[]> => {
+  return post<XxxInfo[]>('/xxx/list', params || {});
+};
+
+/**
+ * 获取xxx详情
+ * @param id xxx ID
+ */
+export const getXxxDetail = (id: string): Promise<XxxInfo> => {
+  return get<XxxInfo>(`/xxx/${id}`);
+};
+```
+
+**禁止事项**:
+- 禁止直接使用 `uni.request`(必须通过 `request.ts` 封装)
+- 禁止在 API 函数中处理 UI 逻辑(如 `uni.showToast`)
+- 禁止使用 `axios`(小程序不支持)
+- 禁止硬编码后端地址(使用 `API_CONFIG.baseUrl`)
+
+### 2.3 请求封装 (utils/request.ts)
+
+**已有封装,禁止重写**。关键行为:
+
+- 请求拦截:自动添加 `accessToken` 请求头
+- 响应拦截:`code === 200 || code === 0` 为成功,返回 `data` 字段
+- 401 处理:清除 token + 1.5s 后跳转登录页
+- 错误处理:自动 `uni.showToast` 提示
+- 支持 `skipAuth` 参数跳过认证(仅用于免登录接口)
+
+**免登录接口示例**:
+```ts
+export const getDeviceProducts = (deviceId: string): Promise<DeviceProductsResponse> => {
+  return get<DeviceProductsResponse>(`/device/products/${deviceId}`, undefined, { skipAuth: true });
+};
+```
+
+### 2.4 认证工具 (utils/auth.ts)
+
+**已有封装,禁止重写**。关键行为:
+
+| 函数 | 用途 |
+|------|------|
+| `getToken()` | 获取 accessToken |
+| `setToken(token)` | 存储 accessToken |
+| `removeToken()` | 清除 accessToken |
+| `isLoggedIn()` | 判断是否已登录 |
+| `checkAuth(redirectUrl?)` | 路由守卫,未登录跳转登录页 |
+| `clearAuth()` | 清除所有登录信息 + 购物流程数据 |
+| `logout()` | 清除登录信息 + reLaunch 到登录页 |
+
+**Token 存储 key**:`accessToken`(uni.getStorageSync)
+
+---
+
+## 3. 样式规则
+
+### 3.1 Design Token 变量
+
+**必须使用 `uni.scss` 中定义的 Design Token**,禁止硬编码:
+
+```scss
+/* 主色系 */
+$color-primary: #FFC107;          // 品牌黄
+$color-primary-light: #FFE082;
+$color-primary-dark: #FFA000;
+
+/* 文字颜色 */
+$color-text-primary: #2C2C2C;     // 主文字
+$color-text-secondary: #8C8C8C;   // 辅助文字
+$color-text-tertiary: #BDBDBD;    // 三级文字
+
+/* 背景色 */
+$color-bg-primary: #FFFFFF;
+$color-bg-secondary: #FAFAFA;
+$color-bg-tertiary: #F5F5F5;
+
+/* 边框/阴影 */
+$color-border: #EEEEEE;
+$shadow-primary: 0 8rpx 24rpx rgba(255, 193, 7, 0.25);
+
+/* 间距系统 */
+$spacing-xs: 8rpx;
+$spacing-sm: 16rpx;
+$spacing-md: 24rpx;
+$spacing-lg: 32rpx;
+$spacing-xl: 48rpx;
+$spacing-xxl: 64rpx;
+
+/* 圆角 */
+$radius-sm: 8rpx;
+$radius-md: 16rpx;
+$radius-lg: 24rpx;
+$radius-xl: 32rpx;
+
+/* 动画 */
+$duration-fast: 150ms;
+$duration-normal: 300ms;
+$ease-out: cubic-bezier(0.25, 0.1, 0.25, 1);
+$bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);
+```
+
+### 3.2 样式编写规范
+
+| 规则 | 说明 |
+|------|------|
+| 单位 | 统一使用 `rpx`(设计稿 750rpx 宽度),禁止 `px`/`rem`/`em` |
+| 颜色 | 使用 Design Token 变量,禁止硬编码颜色值 |
+| 间距 | 使用 `$spacing-xxx` 变量 |
+| 圆角 | 使用 `$radius-xxx` 变量 |
+| 阴影 | 使用 `$shadow-xxx` 变量 |
+| 动画 | 使用 `$duration-xxx` + `$ease-out` |
+| 作用域 | 全局样式不加 scoped,页面级样式尽量加 `scoped` |
+| 性能 | 动画元素添加 `will-change: transform, opacity` + `transform: translateZ(0)` |
+
+### 3.3 禁止事项
+
+- 禁止使用 `px` 作为布局单位(仅 `rpx`)
+- 禁止硬编码颜色值(必须使用变量)
+- 禁止使用 CSS `filter` 属性(小程序性能差)
+- 禁止使用 `backdrop-filter`(兼容性问题)
+- 禁止使用 `position: fixed` 在列表项中(性能问题)
+- 禁止复杂 `box-shadow` 动画
+
+---
+
+## 4. 路由与导航规则
+
+### 4.1 路由方式
+
+| 场景 | API | 说明 |
+|------|-----|------|
+| 普通跳转 | `uni.navigateTo` | 可返回,最多 10 层栈 |
+| 不可返回跳转 | `uni.reLaunch` | 关闭所有页面,打开目标页 |
+| Tab 切换 | `uni.switchTab` | 用于 tabBar 页面(当前无 tabBar) |
+| 返回上一页 | `uni.navigateBack` | -- |
+| 重定向 | `uni.redirectTo` | 关闭当前页,打开目标页 |
+
+### 4.2 路由传参
+
+```ts
+// 传递参数
+uni.navigateTo({
+  url: '/pages/xxx/xxx?id=' + String(id) + '&name=' + name
+});
+
+// 接收参数
+onLoad((options: any) => {
+  const id = options.id;  // 字符串
+});
+```
+
+**强制规则**:
+- 雪花 ID 传参必须 `String(id)` 防止精度丢失
+- 登录页支持 `redirect` 参数:`/pages/login/login?redirect=/pages/index/index`
+
+### 4.3 页面白名单
+
+以下页面允许未登录用户直接访问(定义在 `App.vue`):
+
+```ts
+const LOGIN_WHITE_LIST = [
+  'pages/login/login',
+  'pages/products/products',  // 柜内商品展示
+];
+```
+
+新增免登录页面时,必须同步更新此白名单。
+
+---
+
+## 5. 业务规则约束
+
+### 5.1 扫码开门流程
+
+```
+用户扫码 → 检查登录 → 检查支付分 → 调用 scanDoor API → 存储 deviceId/outTradeNo/orderNo → 跳转购物页
+```
+
+**强制规则**:
+- 扫码前必须检查登录状态
+- 扫码前必须检查微信支付分开通状态
+- 二维码格式:`https://hh.hahabianli.com/B142977?_wxpmm0=...`
+- 设备 ID 提取:正则 `/\/([A-Z0-9]+)(\?|$)/`
+
+### 5.2 订单金额显示
+
+- **实付金额字段**:`paidAmount || totalAmount`(优先使用 paidAmount)
+- 格式化:`(amount || 0).toFixed(2)`
+- 前缀:`¥`
+
+### 5.3 退款时效
+
+- 仅已完成订单(`status === 1`)可申请退款
+- 退款期限:支付时间后 7 天内
+- 超过 7 天自动隐藏退款按钮
+
+### 5.4 登录认证
+
+- **唯一登录方式**:微信手机号快速登录(`open-type="getPhoneNumber"`)
+- API:`/login/miniapp-phone`,参数 `{ code, phoneCode }`
+- 登录成功后存储 token + userInfo
+- 支持登录后跳转回原页面(redirect 参数)
+
+---
+
+## 6. 小程序特有约束
+
+### 6.1 兼容性规则
+
+| 编号 | 约束 | 说明 |
+|------|------|------|
+| C1 | 禁止 DOM API | 无 `document`/`window`/`localStorage` |
+| C2 | 禁止 axios | 小程序不支持 XHR,必须用 `uni.request` |
+| C3 | 图片必须指定 mode | `<image mode="aspectFit/aspectFill">` |
+| C4 | 禁止 v-html | 小程序不支持 |
+| C5 | 禁止动态组件 | `<component :is>` 有限制 |
+| C6 | 使用 rpx 单位 | 适配不同屏幕宽度 |
+| C7 | 事件使用 @tap | 优先使用 `@tap` 代替 `@click` |
+| C8 | 按钮反馈 | 操作按钮添加 `hover-class` |
+| C9 | 安全区适配 | 底部 `padding-bottom: env(safe-area-inset-bottom)` |
+| C10 | 包体积 | 单个包不超过 2MB,主包不超过 2MB |
+
+### 6.2 性能优化规则
+
+| 编号 | 规则 | 说明 |
+|------|------|------|
+| P1 | 动画使用 `transform` + `opacity` | 避免触发 layout/paint |
+| P2 | 动画元素添加 `will-change` | `will-change: transform, opacity` |
+| P3 | 动画元素添加 `transform: translateZ(0)` | 开启 GPU 加速 |
+| P4 | 禁止 `filter: blur()` | 小程序性能极差 |
+| P5 | 禁止复杂 `box-shadow` 动画 | 改用静态阴影 |
+| P6 | 长列表使用虚拟列表 | 超过 100 项必须虚拟滚动 |
+| P7 | 图片懒加载 | `<image lazy-load>` |
+| P8 | 轮询必须有超时和清理 | 避免页面切换后持续请求 |
+
+### 6.3 小程序 API 使用规范
+
+| 需求 | 使用 | 禁止 |
+|------|------|------|
+| 存储 | `uni.getStorageSync/setStorageSync` | `localStorage` |
+| 路由 | `uni.navigateTo/reLaunch` | `router.push` |
+| 提示 | `uni.showToast/showLoading/showModal` | 自定义 toast |
+| 扫码 | `uni.scanCode` | 第三方扫码 |
+| 拨号 | `uni.makePhoneCall` | `tel:` 链接 |
+| 震动 | `uni.vibrateShort` | -- |
+| 登录 | `uni.login` | -- |
+
+---
+
+## 7. 常见陷阱
+
+| 编号 | 陷阱 | 规则 |
+|------|------|------|
+| T1 | 雪花 ID 使用 number 类型 | 禁止。必须为 `string` 类型,防止 JS 精度丢失 |
+| T2 | 路由传参直接传 number ID | 禁止。必须 `String(id)` |
+| T3 | 使用 `localStorage` | 禁止。必须 `uni.getStorageSync/setStorageSync` |
+| T4 | 使用 `axios` | 禁止。小程序不支持,使用封装的 `request.ts` |
+| T5 | 使用 `document/window` | 禁止。小程序无 DOM |
+| T6 | 使用 `px` 单位 | 禁止。统一使用 `rpx` |
+| T7 | 硬编码颜色值 | 禁止。使用 `uni.scss` 中的 Design Token |
+| T8 | 新页面不注册 pages.json | 禁止。每个新页面必须在 pages.json 中配置 |
+| T9 | 免登录页面不更新白名单 | 禁止。同步更新 App.vue 中 LOGIN_WHITE_LIST |
+| T10 | 轮询不设置超时 | 禁止。所有轮询必须有 timeout + 页面卸载清理 |
+| T11 | 在 API 层处理 UI 逻辑 | 禁止。API 层只负责请求 |
+| T12 | 使用 `<div>/<span>/<img>` | 禁止。使用 `<view>/<text>/<image>` |
+| T13 | 导航栏颜色不一致 | 禁止。统一 `#FFD700` 背景 + `black` 文字 |
+| T14 | 实付金额取 totalAmount | 禁止。优先取 `paidAmount`,fallback `totalAmount` |
+| T15 | 不处理安全区 | 禁止。底部内容必须适配 `safe-area-inset-bottom` |

+ 479 - 0
系统交互流程.md

@@ -0,0 +1,479 @@
+# 系统交互流程
+
+## 1. 核心购物流程
+
+用户通过小程序扫码开门、选购商品、自动结算的完整交互流程。
+
+```mermaid
+sequenceDiagram
+    participant 用户
+    participant 小程序 as haha-mp (小程序)
+    participant 后端 as haha-miniapp (后端)
+    participant SDK as HahaClient (SDK)
+    participant 哈哈零兽 as 哈哈零兽平台
+    participant 微信 as 微信支付分
+
+    rect rgb(240, 248, 255)
+        Note over 用户,微信: 阶段一:扫码开门
+        用户->>小程序: 扫描设备二维码
+        小程序->>小程序: 检查登录状态
+        小程序->>后端: GET /api/payscore/check-enable
+        后端-->>小程序: 支付分开通状态
+        alt 未开通支付分
+            小程序->>小程序: 跳转支付分开通页面
+            小程序->>后端: POST /api/payscore/enable
+            后端->>微信: 创建支付分服务订单
+            微信-->>后端: 返回 outOrderNo
+            后端-->>小程序: 返回开通参数
+            小程序->>微信: wx.openBusinessView 调起授权
+            微信-->>小程序: 用户授权结果
+            小程序->>后端: POST /api/payscore/confirm-enable
+            后端-->>小程序: 开通确认结果
+        end
+        小程序->>小程序: 弹窗选择:查看柜内商品 / 直接开门
+        alt 直接开门
+            小程序->>后端: POST /api/device/scan-open {deviceId}
+            后端->>SDK: openDoor(deviceId, userId, OUT, doorIndex, MINIAPP)
+            SDK->>哈哈零兽: POST /door/open
+            哈哈零兽-->>SDK: {activity_id, user_id}
+            SDK-->>后端: OpenDoorResult
+            后端->>后端: 保存开门记录(数据库 + Redis)
+            后端-->>小程序: Result<OpenDoorVO>
+            小程序->>小程序: 跳转购物页面
+        end
+    end
+
+    rect rgb(255, 248, 240)
+        Note over 用户,微信: 阶段二:选购等待
+        小程序->>小程序: 开始轮询设备状态
+        loop 每 2 秒轮询
+            小程序->>后端: GET /api/device/status?deviceId=xxx
+            后端-->>小程序: 设备门状态
+        end
+        哈哈零兽->>后端: 回调 DEVICE_STATUS (status=OPENED)
+        后端->>后端: 保存状态到 Redis + 更新数据库门状态
+        Note over 用户: 用户选购商品...
+        哈哈零兽->>后端: 回调 DEVICE_STATUS (status=CLOSED)
+        后端->>后端: 更新开门记录关门状态 + 更新数据库门状态
+    end
+
+    rect rgb(240, 255, 240)
+        Note over 用户,微信: 阶段三:AI识别
+        哈哈零兽->>后端: 回调 ORC_RESULT (AI识别结果)
+        后端->>后端: 保存识别结果到 Redis
+        alt 有消费 (nobuy=0)
+            后端->>后端: 获取或创建订单
+            后端->>后端: 更新订单商品列表 + 视频URL(金额待订单回调更新)
+            后端->>后端: 更新关门状态
+        else 无消费 (nobuy=1)
+            后端->>后端: 标记无消费 + 更新关门状态
+        end
+        后端-->>哈哈零兽: "success"
+    end
+
+    rect rgb(255, 240, 255)
+        Note over 用户,微信: 阶段四:订单结算
+        哈哈零兽->>后端: 回调 ORDER (订单信息 + 金额)
+        后端->>后端: 更新订单号 + 订单金额 + 订单类型
+        后端->>后端: 保存订单商品明细
+        后端->>后端: 优惠券自动扣减(最先到期优先)
+        后端->>后端: 更新优惠金额 + 实付金额
+        alt 支付分订单
+            后端->>微信: completePayScoreOrder (完结扣费)
+            微信-->>后端: 扣费结果
+        end
+        后端->>后端: 保存订单信息到 Redis
+        后端-->>哈哈零兽: "success"
+        小程序->>后端: 轮询获取订单信息
+        后端-->>小程序: 返回订单详情
+        小程序->>小程序: 显示订单结果
+    end
+```
+
+## 2. 回调通知体系
+
+哈哈零兽平台通过两类回调地址推送通知,后端统一在 [CallbackController](haha-miniapp/src/main/java/com/haha/miniapp/controller/CallbackController.java) 接收处理。
+
+### 2.1 消息回调 (`/callback/haha/message`)
+
+| notify_type | 说明 | 处理逻辑 |
+|-------------|------|----------|
+| DEVICE_STATUS | 开关门状态通知 | 更新 Redis 缓存 + 数据库门状态 + 开门记录 |
+| ONLINE_STATUS | 设备在线状态通知 | 更新 Redis 缓存 + 离线告警/上线恢复/弱信号告警 |
+| VOICE_RESULT | 音量调节结果通知 | 更新 Redis 缓存 |
+| ORC_RESULT | AI识别结果通知 | 保存识别结果 + 创建/更新订单 + 关门状态处理 |
+| CLIENT_NEW_PRODUCT | 新品审核结果回调 | 更新申请记录 + 同步商品到商家库 |
+| MERGE_PRODUCT | 商品合并结果通知 | 记录合并日志 |
+
+### 2.2 订单回调 (`/callback/haha/order`)
+
+哈哈零兽生成订单后推送的完整订单信息,包含订单号、金额、商品明细等。
+
+### 2.3 回调处理流程
+
+```mermaid
+flowchart TD
+    Callback[收到回调通知] --> VerifySign[验证签名]
+    VerifySign --> ParseParams[解析回调参数]
+    ParseParams --> CheckType{判断回调类型}
+
+    CheckType --> |消息回调| CheckNotifyType{notify_type?}
+    CheckType --> |订单回调| HandleOrder[处理订单回调]
+
+    CheckNotifyType --> |DEVICE_STATUS| HandleDeviceStatus[处理开关门状态]
+    CheckNotifyType --> |ONLINE_STATUS| HandleOnlineStatus[处理在线状态]
+    CheckNotifyType --> |VOICE_RESULT| HandleVoiceResult[处理音量结果]
+    CheckNotifyType --> |ORC_RESULT| HandleOrcResult[处理AI识别结果]
+    CheckNotifyType --> |CLIENT_NEW_PRODUCT| HandleNewProduct[处理新品审核]
+    CheckNotifyType --> |MERGE_PRODUCT| HandleMergeProduct[处理商品合并]
+
+    HandleDeviceStatus --> SaveRedis1[保存状态到Redis]
+    SaveRedis1 --> UpdateDB1[更新数据库门状态]
+    UpdateDB1 --> UpdateRecord1[更新开门记录]
+
+    HandleOnlineStatus --> SaveRedis2[保存在线状态到Redis]
+    SaveRedis2 --> CheckOnline{在线/离线?}
+    CheckOnline --> |离线| OfflineAlert[触发离线告警]
+    CheckOnline --> |在线| BackOnline[发送恢复通知]
+    BackOnline --> CheckSignal{信号强度?}
+    CheckSignal --> |弱信号| WeakSignalAlert[触发弱信号告警]
+
+    HandleOrcResult --> SaveRedis3[保存识别结果到Redis]
+    SaveRedis3 --> CheckConsume{是否有消费?}
+    CheckConsume --> |有消费| CreateOrder[创建/更新订单]
+    CheckConsume --> |无消费| MarkNoConsume[标记无消费]
+
+    HandleOrder --> UpdateOrderAmount[更新订单金额]
+    UpdateOrderAmount --> SaveOrderGoods[保存订单商品]
+    SaveOrderGoods --> CouponDiscount[优惠券自动扣减]
+    CouponDiscount --> PayScorePayment[支付分扣费]
+    PayScorePayment --> SaveRedis4[保存订单到Redis]
+
+    RecordResult[返回 success]
+    HandleVoiceResult --> SaveRedis5[保存音量到Redis] --> RecordResult
+    HandleNewProduct --> UpdateApply[更新申请记录] --> SyncProduct[同步商品] --> RecordResult
+    HandleMergeProduct --> LogMerge[记录合并日志] --> RecordResult
+    MarkNoConsume --> RecordResult
+    CreateOrder --> RecordResult
+    SaveRedis4 --> RecordResult
+    OfflineAlert --> RecordResult
+    BackOnline --> RecordResult
+    WeakSignalAlert --> RecordResult
+    UpdateDB1 --> RecordResult
+```
+
+## 3. 扫码开门详细流程
+
+```mermaid
+sequenceDiagram
+    participant 小程序
+    participant 后端 as DeviceServiceImpl
+    participant SDK as HahaClient
+    participant 哈哈零兽
+    participant Redis
+    participant DB as 数据库
+
+    小程序->>后端: scanOpenDoor(deviceId, userId)
+    后端->>后端: checkUserPayscoreStatus(userId)
+    后端->>SDK: getOnlineStatus(deviceId)
+    SDK->>哈哈零兽: 查询设备在线状态
+    哈哈零兽-->>SDK: DeviceOnlineStatus
+    alt 设备离线
+        SDK-->>后端: isOnline=0
+        后端-->>小程序: BusinessException("设备当前离线")
+    end
+    后端->>SDK: isMultiDoorUnique(deviceId)
+    SDK-->>后端: 多门单开类型
+    alt 多门单开设备
+        后端->>后端: doorIndex = "A"
+    end
+    后端->>SDK: openDoor(deviceId, userId, OUT, doorIndex, MINIAPP)
+    SDK->>哈哈零兽: POST /door/open
+    哈哈零兽-->>SDK: {activity_id, user_id}
+    SDK-->>后端: OpenDoorResult
+    后端->>DB: 保存开门记录 (DoorRecord)
+    后端->>Redis: 保存设备状态
+    后端-->>小程序: OpenDoorVO
+```
+
+## 4. 订单结算流程
+
+订单回调触发完整的结算链路,包括金额更新、优惠券扣减、支付分扣费。
+
+```mermaid
+flowchart TD
+    OrderCallback[收到订单回调] --> FindOrder[查找本地订单]
+    FindOrder --> OrderExists{订单是否存在?}
+    OrderExists --> |是| CheckDuplicate{是否已处理?}
+    OrderExists --> |否| FetchRecognition[主动查询识别结果创建订单]
+
+    CheckDuplicate --> |已处理| SaveRedis[保存到Redis]
+    CheckDuplicate --> |未处理| UpdateOrder[更新订单信息]
+
+    FetchRecognition --> CreateFromRecognition[根据识别结果创建订单]
+    CreateFromRecognition --> UpdateOrder
+
+    UpdateOrder --> SetAmount[设置订单金额 totalAmount]
+    SetAmount --> InitDiscount[初始化 discountAmount=0, paidAmount=totalAmount]
+    InitDiscount --> SaveGoods[保存订单商品明细]
+    SaveGoods --> ProcessCoupon[优惠券自动扣减]
+
+    ProcessCoupon --> QueryCoupons[查询用户可用优惠券]
+    QueryCoupons --> SortByExpiry[按到期时间升序排序]
+    SortByExpiry --> FindApplicable{找到适用优惠券?}
+    FindApplicable --> |是| CalcDiscount[计算优惠金额]
+    FindApplicable --> |否| UseOriginalAmount[使用原金额]
+
+    CalcDiscount --> CheckCap{优惠金额 > 订单金额?}
+    CheckCap --> |是| CapDiscount[优惠金额 = 订单金额]
+    CheckCap --> |否| KeepDiscount[保持计算值]
+    CapDiscount --> FinalAmount
+    KeepDiscount --> FinalAmount[计算实付金额 paidAmount = total - discount]
+    FinalAmount --> UseCoupon[标记优惠券已使用]
+    UseCoupon --> UpdateOrderDiscount[更新订单 discountAmount + paidAmount]
+
+    UseOriginalAmount --> PayScoreCheck
+    UpdateOrderDiscount --> PayScoreCheck{支付分订单?}
+
+    PayScoreCheck --> |是| DeductPayScore[支付分完结扣费]
+    PayScoreCheck --> |否| SaveOrderRedis[保存订单到Redis]
+    DeductPayScore --> SaveOrderRedis
+    SaveOrderRedis --> Done[结算完成]
+```
+
+### 4.1 金额流转说明
+
+| 阶段 | 负责方 | totalAmount | discountAmount | paidAmount |
+|------|--------|-------------|----------------|------------|
+| 开门记录 | DeviceServiceImpl | - | - | - |
+| AI识别回调 | HahaCallbackServiceImpl | 0 | 0 | 0 |
+| 订单回调 | HahaCallbackServiceImpl | 设备传来 | 0 | = totalAmount |
+| 优惠券扣减 | HahaCallbackServiceImpl | 不变 | 优惠券金额 | = total - discount |
+| 支付扣费 | PayScoreService | 不变 | 不变 | 使用此金额扣费 |
+
+### 4.2 优惠券扣减规则
+
+- **排序策略**:按到期时间升序,优先使用最先到期的券
+- **使用限制**:一次订单只能使用一张优惠券
+- **适用范围**:
+  - `applyScope=1`:全场通用
+  - `applyScope=2`:指定门店
+  - `applyScope=3`:指定商品
+- **优惠金额上限**:不超过订单金额
+- **优惠券类型**:
+  - `type=1` 满减券:直接返回 discountValue
+  - `type=2` 折扣券:orderAmount × (1 - discountValue/10),受 maxDiscount 上限约束
+  - `type=3` 抵扣券:直接返回 discountValue
+  - `type=4` 兑换券:不参与金额扣减
+
+## 5. 微信支付分集成流程
+
+```mermaid
+sequenceDiagram
+    participant 用户
+    participant 小程序
+    participant 后端 as PayScoreServiceImpl
+    participant 微信 as 微信支付分
+
+    rect rgb(240, 255, 255)
+        Note over 用户,微信: 支付分开通过程
+        用户->>小程序: 点击开通支付分
+        小程序->>后端: POST /api/payscore/enable
+        后端->>微信: 创建支付分服务订单
+        微信-->>后端: outOrderNo + serviceId
+        后端-->>小程序: 开通参数
+        小程序->>微信: wx.openBusinessView 调起授权页
+        微信-->>用户: 展示授权页面
+        用户->>微信: 确认授权
+        微信-->>小程序: 授权结果回调
+        小程序->>后端: POST /api/payscore/confirm-enable
+        后端->>微信: 查询支付分状态
+        微信-->>后端: 用户支付分状态
+        后端->>后端: 更新用户 payscoreEnabled 状态
+        后端-->>小程序: 开通成功
+    end
+
+    rect rgb(255, 255, 240)
+        Note over 用户,微信: 开门时创建支付分服务订单
+        小程序->>后端: POST /api/device/scan-open
+        后端->>后端: scanOpenDoor()
+        后端->>后端: 创建本地订单
+        后端->>后端: 创建支付分服务订单
+        后端->>微信: createPayScoreOrder(orderId, openId)
+        微信-->>后端: 支付分服务订单号
+        后端->>后端: 保存 payScoreOrderId 到订单
+    end
+
+    rect rgb(255, 240, 240)
+        Note over 用户,微信: 订单完结自动扣费
+        后端->>后端: processPayScorePayment(order, finalAmount)
+        后端->>后端: 校验:支付分渠道 + 服务可用 + 订单未完结
+        后端->>微信: completePayScoreOrder(orderId, finalAmount)
+        微信->>微信: 自动扣款
+        微信-->>后端: 扣款结果
+        后端->>后端: 更新订单支付状态
+    end
+
+    rect rgb(245, 240, 255)
+        Note over 用户,微信: 支付分回调
+        微信->>后端: POST /api/payscore/callback
+        后端->>后端: 解析回调参数
+        后端->>后端: 更新订单支付分状态
+        后端-->>微信: 返回成功响应
+    end
+```
+
+### 5.1 支付分状态流转
+
+```mermaid
+stateDiagram-v2
+    [*] --> CREATED: 创建服务订单
+    CREATED --> DOING: 用户授权确认
+    DOING --> DONE: 完结扣费成功
+    DOING --> REVOKED: 取消订单
+    DONE --> [*]
+    REVOKED --> [*]
+```
+
+## 6. 购物页面状态流转
+
+小程序购物页面的状态轮询与界面联动机制。
+
+```mermaid
+stateDiagram-v2
+    [*] --> opened: 开门成功
+    opened --> opened: 轮询设备状态(2s间隔)
+    opened --> closing: 门已关闭
+    opened --> error: 设备异常
+
+    closing --> closed: 获取到订单信息
+    closing --> error: 订单异常
+
+    closed --> [*]: 倒计时结束返回首页
+    error --> [*]: 返回首页
+```
+
+### 6.1 轮询机制
+
+| 轮询类型 | 间隔 | 超时 | 说明 |
+|----------|------|------|------|
+| 设备状态 | 2秒 | 120秒 | 等待门从 opened 变为 close |
+| 识别结果 | 3秒 | - | 门关后轮询 AI 识别结果 |
+| 订单信息 | 3秒 | - | 识别完成后轮询订单详情 |
+
+### 6.2 购物页生命周期
+
+- **组件激活** (`onActivated`):重新启动轮询
+- **组件失活** (`onDeactivated`):暂停轮询、清理定时器
+- **应用隐藏** (`onHide`):清理购物页面轮询状态
+- **组件卸载**:清理所有定时器和轮询
+
+## 7. 管理后台远程控制
+
+运营平台通过管理后台远程控制设备。
+
+```mermaid
+sequenceDiagram
+    participant 运营人员
+    participant Web管理端 as haha-admin-web
+    participant Admin后端 as haha-admin
+    participant SDK as HahaClient
+    participant 哈哈零兽
+
+    rect rgb(245, 245, 245)
+        Note over 运营人员,哈哈零兽: 远程开门
+        运营人员->>Web管理端: 点击远程开门
+        Web管理端->>Admin后端: POST /admin/devices/{id}/open
+        Admin后端->>Admin后端: 查找设备
+        Admin后端->>SDK: openDoor(deviceId, ADMIN+timestamp, IN, doorIndex)
+        SDK->>哈哈零兽: POST /door/open
+        哈哈零兽-->>SDK: {activity_id}
+        SDK-->>Admin后端: OpenDoorResult
+        Admin后端-->>Web管理端: 操作结果
+    end
+
+    rect rgb(245, 245, 245)
+        Note over 运营人员,哈哈零兽: 设置温度
+        运营人员->>Web管理端: 输入温度值(-30~30℃)
+        Web管理端->>Admin后端: POST /admin/devices/{id}/temperature
+        Admin后端->>SDK: setTemperature(deviceId, temperature)
+        SDK->>哈哈零兽: 设置温度指令
+        SDK-->>Admin后端: 设置结果
+        Admin后端-->>Web管理端: 操作结果
+    end
+
+    rect rgb(245, 245, 245)
+        Note over 运营人员,哈哈零兽: 音量调节
+        运营人员->>Web管理端: 输入音量值(0~100)
+        Web管理端->>Admin后端: POST /admin/devices/{id}/volume
+        Admin后端->>SDK: setVolume(deviceId, volume)
+        SDK->>哈哈零兽: 音量调节指令
+        SDK-->>Admin后端: 设置结果
+        Admin后端-->>Web管理端: 操作结果
+    end
+```
+
+> **注意**:管理后台开门默认为上货模式 (`openType=IN`),`outUserId` 格式为 `ADMIN+时间戳`;小程序开门为消费模式 (`openType=OUT`),`source=MINIAPP`。
+
+## 8. 设备告警体系
+
+设备在线状态回调触发的告警流程。
+
+```mermaid
+flowchart TD
+    OnlineCallback[收到 ONLINE_STATUS 回调] --> ParseOnline[解析在线状态]
+    ParseOnline --> SaveOnlineRedis[保存在线状态到Redis 10min TTL]
+    SaveOnlineRedis --> CheckOnlineStatus{is_online?}
+
+    CheckOnlineStatus --> |0-离线| TriggerOfflineAlert[触发离线告警]
+    CheckOnlineStatus --> |1-在线| CheckBackOnline[检查是否需要恢复通知]
+
+    TriggerOfflineAlert --> AlertService[DeviceAlertService.processOfflineAlert]
+    AlertService --> NotifyWeCom[发送企业微信告警通知]
+
+    CheckBackOnline --> ProcessBackOnline[DeviceAlertService.processBackOnlineNotify]
+    ProcessBackOnline --> SendRecoverNotify[发送恢复通知]
+    SendRecoverNotify --> ExtractSignal[提取信号值 signal_val]
+    ExtractSignal --> CheckSignal{信号强度 <= 阈值?}
+    CheckSignal --> |是| WeakSignalAlert[触发弱信号告警]
+    CheckSignal --> |否| Done[完成]
+    WeakSignalAlert --> NotifyWeCom2[发送企业微信告警通知]
+```
+
+## 9. 系统角色说明
+
+| 角色 | 说明 | 对应模块 |
+|------|------|----------|
+| 用户 | 最终消费者,通过小程序扫码购物 | haha-mp |
+| 运营人员 | 管理后台操作人员,远程控制设备 | haha-admin-web |
+| 哈哈零兽 | 设备平台,负责设备控制、AI识别、订单生成 | 外部平台 (SDK对接) |
+| haha-miniapp | 小程序后端,处理回调、订单结算 | haha-miniapp |
+| haha-admin | 运营平台后端,设备管理、营销管理 | haha-admin |
+| 微信支付分 | 免密支付渠道,先享后付 | 外部平台 (API对接) |
+
+## 10. 关键接口索引
+
+### 10.1 小程序端 API (`/api`)
+
+| 接口 | 方法 | 说明 |
+|------|------|------|
+| `/api/device/scan-open` | POST | 扫码开门 |
+| `/api/payscore/check-enable` | GET | 检查支付分开通状态 |
+| `/api/payscore/enable` | POST | 开通支付分 |
+| `/api/payscore/confirm-enable` | POST | 确认开通支付分 |
+| `/api/payscore/callback` | POST | 支付分回调通知 |
+
+### 10.2 回调接口 (`/callback/haha`)
+
+| 接口 | 方法 | 说明 |
+|------|------|------|
+| `/callback/haha/message` | POST | 消息回调统一入口 |
+| `/callback/haha/order` | POST | 订单回调通知 |
+
+### 10.3 管理端 API (`/admin`)
+
+| 接口 | 方法 | 说明 |
+|------|------|------|
+| `/admin/devices/{id}/open` | POST | 远程开门 |
+| `/admin/devices/{id}/temperature` | POST | 设置温度 |
+| `/admin/devices/{id}/volume` | POST | 音量调节 |