# 系统架构与公共模块说明

> 本文档描述项目整体架构、双应用部署、模块依赖、公共组件及开发规范要点。
> 帮助开发人员快速理解“为什么这样设计”以及各环节的特殊处理方式。

---

## 一、系统架构

### 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 实体

设备货架层配置模板，对应数据库 `t_layer_template` 表。每个设备可有多个层模版（左右门分别管理）。数据来源于哈哈平台 API（`GoodsApi.getLayerTemplate()`）。

| 核心字段 | 说明 |
|---------|------|
| `deviceId` | 设备 SN |
| `templateName` | 模板名称 |
| `deviceType` | 设备类型（1-静态柜, 2-动态柜） |
| `shelfNum` | 机柜层数 |
| `leftFloors` | 左门层数据（JSON，含每层货道商品配置） |
| `rightFloors` | 右门层数据（JSON） |
| `syncStatus` | 同步状态（0-未同步, 1-同步中, 2-已同步, 3-同步失败） |

### 8.2 核心功能

| 功能 | 说明 |
|------|------|
| 模板 CRUD | 创建/编辑/删除层模板 |
| 层配置 | 每层的商品类型、数量、位置 |
| 同步拉取 | 从哈哈平台拉取最新层模版配置（支持单设备/批量） |
| 库存联动 | 同步后自动初始化库存记录 + 重算标准库存（详见下方） |

### 8.3 同步与库存联动

层模版同步不仅仅更新 `t_layer_template` 表，还会联动操作 `t_device_inventory` 表：

```
syncFromHaha(deviceId) / batchSync(deviceIds)
  │
  ├─ ① 调用 haha API 拉取层模版 JSON
  ├─ ② 解析并写入 t_layer_template（INSERT 或 UPDATE）
  ├─ ③ initInventoryFromTemplate() → 为缺失商品建库存记录(stock=0)
  └─ ④ recalculateStandardStock() → 按货道配置重算 standard_stock
```

**步骤③**：遍历层模版中的商品编码，对库存表中不存在的商品创建 `stock=0` 的占位记录，让运营平台提前看到设备所有理论商品。
**步骤④**：遍历层模版中每个货道的商品配置，按 productCode 累加摆放数量，写入对应库存记录的 `standard_stock` 字段，用于前端展示库存占比。

> 详细逻辑见 [库存管理模块 §1.4-1.5](./库存管理模块.md)。

---

## 九、开发规范要点

1. **Long类型序列化**：所有 Long 类型 ID 和外键字段必须添加 `@JsonSerialize(using = ToStringSerializer.class)` 防精度丢失
   - **原因**：JavaScript 的 Number 类型最大安全整数为 2^53-1，超过则精度丢失。雪花算法生成的ID约19位数字，远超此限制
   - **影响**：不添加此注解，前端接收到的ID末尾会被截断为0，导致根据ID查询/更新失败
2. **日期格式**：`@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")`
3. **金额字段**：必须使用 `BigDecimal`，禁止 `double` / `float`
   - **原因**：浮点数在计算机中无法精确表示十进制小数，如 0.1 + 0.2 != 0.3
   - **运算**：使用 `setScale(2, RoundingMode.HALF_UP)` 保留2位小数
4. **依赖注入**：构造器注入（`final` + `@RequiredArgsConstructor`），循环依赖时用 `@Autowired @Lazy`
5. **事务**：写操作必须 `@Transactional(rollbackFor = Exception.class)`
   - **注意**：`rollbackFor = Exception.class` 不能省略，默认只回滚 RuntimeException
6. **分页校验**：`page < 1` 设为 1，`pageSize > 100` 设为 10
7. **业务异常**：使用 `BusinessException` + `ResponseEnum`，禁止 Controller 中抛出未处理异常
8. **雪花算法ID注意**：手写SQL中的逻辑删除条件不会自动添加，需手动加 `WHERE deleted = 0`

---

## 十、关键设计决策记录

### 10.1 为什么用雪花算法而非自增ID？

系统需要与哈哈平台（设备端）交互，设备端生成 `activityId` 关联开关门和订单。自增ID依赖数据库生成，在分布式场景和回调场景下不可用。雪花算法在应用层生成ID，不依赖数据库，且全局唯一。

**迁移注意**：项目早期使用自增ID，后改为雪花算法。已有数据需保留原ID，新数据使用雪花算法。`alter_snowflake_id.sql` 处理了迁移逻辑。

### 10.2 为什么 haha-sdk 是独立模块？

`haha-sdk` 封装与哈哈平台的HTTP交互，不依赖任何内部业务模块。这样设计是因为：
- SDK可独立发布给第三方使用
- 避免循环依赖（如果SDK依赖service，service又调用SDK）
- SDK的更新不影响业务模块的编译

### 10.3 MyBatis-Plus 逻辑删除的特殊处理

MyBatis-Plus 的 `@TableLogic` 注解在 MP 自动生成的 SQL 中会自动添加 `WHERE deleted = 0`，但**手写SQL（XML或注解式）不会自动添加**。开发手写SQL时必须手动处理逻辑删除条件，否则会查询到已删除的数据。
