21 KiB
后端模块开发指南
从广州广告监管系统(OARMS)各模块开发中提炼的完整流程、约定和踩坑记录。 适用于所有新模块(AM / BS / CW / LB / MR)的后端开发。
开发模式:将 mock 数据插入到数据库中,不使用 mock 数据,直连达梦 DM8 数据库读写,所有接口面向生产环境数据。
A. 运行环境
A1. 技术栈
| 组件 | 版本 | 说明 |
|---|---|---|
| JDK | 21 | JAVA_HOME="/d/Program Files/Java/jdk-21.0.11+10" |
| Spring Boot | 3.4.5 | + youfool-framework-springboot3 1.0.4(公司内部框架) |
| MyBatis-Plus | - | ORM,分页插件配置在 mybatis/mybatis-config.xml |
| Sa-Token | - | 认证鉴权,拦截规则在 SpringMvcConfig |
| dynamic-datasource | baomidou | 多数据源切换 |
| Knife4j | - | API 文档,路径 /doc.html |
| Druid | - | 连接池 + 监控,路径 /druid |
A2. 数据源
项目使用苞米豆动态数据源,两个数据源:
| 数据源 | 用途 | 数据库名 |
|---|---|---|
master(primary) |
业务库 | OARMS |
youfool |
框架库(restLog 等) | YOUFOOL |
实体类注解:@DS("master") + @TableName(schema = "OARMS", value = "表名")
A3. 数据库连接信息(达梦 DM8)
JDBC URL: jdbc:dm://172.22.80.70:15236?schema=OARMS
用户名: SYSDBA
密码: chinaweal
驱动类: dm.jdbc.driver.DmDriver
驱动 JAR: target/libs/DmJdbcDriver18-8.1.3.62.jar
A4. Maven 配置
- Maven 路径:
/d/apache-maven-3.9.15/bin/mvn - settings.xml:
/d/apache-maven-3.9.15/conf/settings.xml - 本地仓库:
D:/maven-repository - Profile:
-Pcompany-nexus(公司私有仓库http://121.8.152.130:8081/nexus/content/groups/public/)
A5. 应用配置
- 默认端口:8080,环境变量
PRJ_PORT可覆盖 - 环境切换:
spring.profiles.active设为dev或prod - Mapper 扫描:
com.chinaweal.youfool.prj.**.mapper(新模块 mapper 放在此包下自动注册) - XML 加载:
classpath*:mybatis/mapper/**/*.xml
A6. 认证鉴权
- Sa-Token 拦截器注册在
SpringMvcConfig,默认所有路径需登录 - 白名单(免登录):
/user/auth/**、/test/**、/doc.html**、/cms/index.html等
B. 工具
B1. 编译命令
# 开发阶段编译(跳过 checkstyle)
JAVA_HOME="/d/Program Files/Java/jdk-21.0.11+10" \
/d/apache-maven-3.9.15/bin/mvn compile -Pcompany-nexus -Dcheckstyle.skip=true
# 完整编译(含 checkstyle)
JAVA_HOME="/d/Program Files/Java/jdk-21.0.11+10" \
/d/apache-maven-3.9.15/bin/mvn compile -Pcompany-nexus
强制重新编译指定模块(跳过 clean):
rm -rf target/classes/com/chinaweal/youfool/prj/modules/{module-name}/
JAVA_HOME="/d/Program Files/Java/jdk-21.0.11+10" \
/d/apache-maven-3.9.15/bin/mvn compile -Pcompany-nexus -Dcheckstyle.skip=true
B2. 数据库执行工具:Python + JayDeBeApi
用于执行 DDL 和初始数据到 DM8:
import jaydebeapi
conn = jaydebeapi.connect(
'dm.jdbc.driver.DmDriver',
'jdbc:dm://172.22.80.70:15236?schema=OARMS',
['SYSDBA', 'chinaweal'],
'target/libs/DmJdbcDriver18-8.1.3.62.jar'
)
cursor = conn.cursor()
# CREATE TABLE(小写表名 OK,DM8 自动转大写)
cursor.execute("CREATE TABLE OARMS.xxx (...)")
# COMMENT / INDEX(必须大写表名列名)
cursor.execute("COMMENT ON TABLE OARMS.XXX IS '注释'")
cursor.execute("CREATE INDEX IDX_XXX_COL ON OARMS.XXX (COL)")
# INSERT(列名小写 OK)
cursor.execute("INSERT INTO OARMS.xxx (...) VALUES (...)")
conn.commit()
# 验证
cursor.execute("SELECT COUNT(*) FROM OARMS.XXX")
print(cursor.fetchone()[0])
cursor.close()
conn.close()
B3. SQL 版本号分配
DDL 文件统一放在 docs/db/sql/,命名规则 V{版本号}__{模块描述}_{ddl|init_data}.sql。
| 版本 | 用途 | 文件 |
|---|---|---|
| V1.0.0 | BS 大屏基础信息管理 | V1.0.0__BS_screen_ddl.sql + _init_data.sql |
| V2.0.0 | LB 法律法规管理 | V2.0.0__LB_law_ddl.sql + _init_data.sql |
| V3.0.0 | AM-1 广告画面录屏设置管理 | V3.0.0__AM_recording_config_ddl.sql |
| V4.0.0 | AM-2 广告画面随机录屏 | V4.0.0__AM_recording_task_ddl.sql |
| V5.0.0 | AM-3 广告画面监控 | V5.0.0__AM_monitor_record_ddl.sql |
| V6.0.0 | MR-1 监测规则管理 | V6.0.0__MR_monitoring_rule_ddl.sql + _init_data.sql |
| V7.0.0 | CW-1 广告监测固化取证 | V7.0.0__CW_evidence_ddl.sql |
| V8.0.0 | CW-2 监测规则关联 | V8.0.0__CW_evidence_rule_relation_ddl.sql |
| V9.0.0 | CW-3 监测线索生成 | V9.0.0__CW_monitoring_clue_ddl.sql |
| V10.0.0 | CW-4 对接线索转办 | V10.0.0__CW_clue_transfer_ddl.sql |
C. 记忆
开发每个模块时必须遵循的模式、约定和模板。直接复制使用,不要重新设计。
C1. 表前缀约定
| 业务域 | 前缀 | Java 包名 |
|---|---|---|
| 大屏基础库 | bs_ |
screen |
| 法律法规库 | lb_ |
law |
| 广告监控 | am_ |
monitor |
| 监测规则库 | mr_ |
rule |
| 内容预警 | cw_ |
evidence |
C2. DM8 类型 → Java 映射
| DM8 类型 | Java 类型 | 场景 |
|---|---|---|
VARCHAR(50) |
String |
主键 ID(ASSIGN_UUID) |
VARCHAR(20~200) |
String |
名称、编码 |
VARCHAR(500~1000) |
String |
描述、地址 |
CLOB |
String |
大文本、JSON(规则配置、快照) |
TINYINT |
Boolean 或 Integer |
0/1 标志位 |
DECIMAL(10,6) |
BigDecimal |
经纬度(精度 ≤ 38) |
INT |
Integer |
数量、排序号 |
DATE |
LocalDate |
仅日期 |
TIMESTAMP |
LocalDateTime |
日期 + 时间 |
C3. 包结构模板
modules/{module-name}/
├── entity/
│ ├── {MainEntity}.java
│ ├── {SubEntity}.java
│ ├── query/
│ │ └── {Entity}Query.java # 分页 + 筛选
│ ├── req/
│ │ ├── {Entity}SaveReq.java # 新增/修改
│ │ └── {Action}Req.java # 特殊操作
│ └── vo/
│ └── {Entity}DetailVO.java # 详情聚合
├── mapper/
│ └── {Entity}Mapper.java
├── service/
│ ├── I{Entity}Service.java
│ └── impl/
│ └── {Entity}ServiceImpl.java
└── controller/
└── {Entity}Controller.java
C4. Entity 模板
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@DS("master")
@TableName(schema = "OARMS", value = "bs_screen")
public class ScreenEntity extends SuperEntity {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.ASSIGN_UUID)
private String id;
@TableField("screen_name")
private String screenName;
}
SuperEntity提供 6 个审计字段(createBy/createTime/createName/updateBy/updateTime/updateName),不含 id- 日期字段加
@JsonFormat(pattern = DateUtil.DATE_DEFAULT_FORMAT, timezone = "GMT+8") - 日期时间字段加
@JsonFormat(pattern = DateUtil.DATETIME_DEFAULT_FORMAT, timezone = "GMT+8")
C5. Controller 模板
@Slf4j
@RestController
@RequestMapping("/api/{resource-path}")
@AllArgsConstructor
@Tag(name = "模块名称", description = "模块描述")
public class XxxController {
private final IXxxService xxxService;
@PostMapping("/query")
@Operation(summary = "分页查询")
public RestResult<IPage<Xxx>> queryList(@RequestBody XxxQuery query) {
log.info("[OK] 查询列表: key={}", query.getKey());
return RestResult.ok(xxxService.queryList(query));
}
@GetMapping("/detail")
@Operation(summary = "查询详情")
public RestResult<XxxDetailVO> getDetail(@RequestParam String id) {
log.info("[OK] 查询详情: id={}", id);
return RestResult.ok(xxxService.getDetail(id));
}
@PostMapping("/save")
@Operation(summary = "新增")
public RestResult<String> save(@RequestBody XxxSaveReq req) {
return RestResult.ok(xxxService.save(req));
}
@PostMapping("/update")
@Operation(summary = "修改")
public RestResult<Void> update(@RequestBody XxxSaveReq req) {
xxxService.update(req);
return RestResult.ok();
}
@PostMapping("/remove")
@Operation(summary = "删除")
public RestResult<Void> remove(@RequestBody Map<String, String> params) {
xxxService.remove(params.get("id"));
return RestResult.ok();
}
@GetMapping("/xxx/list")
@Operation(summary = "选项列表")
public RestResult<List<Map<String, String>>> listXxxOptions() {
return RestResult.ok(xxxService.getXxxOptions());
}
}
- DI:
@AllArgsConstructor+private final - 返回值:
RestResult.ok(data)/RestResult.ok() - 日志:
log.info("[OK] 操作描述: key={}", value) - 删除用
Map<String, String>接收 id - 选项接口返回
List<Map<String, String>>(label + value)
C6. Service 模板
@Slf4j
@Service
public class XxxServiceImpl extends ServiceImpl<XxxMapper, XxxEntity>
implements IXxxService {
@Override
public IPage<XxxEntity> queryList(XxxQuery query) {
Page<XxxEntity> page = new Page<>(query.getPageNum(), query.getPageSize());
QueryWrapper<XxxEntity> wrapper = Wrappers.query();
if (query.getName() != null && !query.getName().isEmpty()) {
wrapper.like("name", query.getName());
}
wrapper.orderByAsc("name");
return baseMapper.selectPage(page, wrapper);
}
@Override
@DSTransactional
public String save(XxxSaveReq req) {
XxxEntity entity = new XxxEntity();
BeanUtils.copyProperties(req, entity, "id");
save(entity);
return entity.getId();
}
}
- 事务必须用
@DSTransactional BeanUtils.copyProperties(req, entity, "id")第三个参数忽略 id- 分页用
new Page<>(pageNum, pageSize) - 查询用
QueryWrapper或LambdaQueryWrapper
C7. Mapper 模板
@Mapper
public interface XxxMapper extends BaseMapper<XxxEntity> {
}
无需 XML,MyBatis-Plus 自动提供 CRUD。新模块 mapper 放在 com.chinaweal.youfool.prj.**.mapper 包下自动注册。
C8. Query 模板
@Data
@Accessors(chain = true)
@Schema(description = "Xxx查询参数")
public class XxxQuery implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "页码")
private Integer pageNum = 1;
@Schema(description = "每页条数")
private Integer pageSize = 10;
@Schema(description = "排序字段")
private List<String> orderFields;
@Schema(description = "排序规则")
private List<String> orderSorts;
// ... 筛选条件
}
C9. 对象映射选择
| 方面 | MapStruct | BeanUtils |
|---|---|---|
| 依赖 | 需要 convert/ 接口 |
Spring 自带 |
| 编译警告 | unmapped properties 警告 | 无 |
| 适用场景 | 字段多、映射复杂 | 字段少、映射简单 |
| 建议 | 已有模块保持 | 新模块推荐 BeanUtils,减少样板代码 |
C10. 初始数据约定
系统运行必需的种子数据(码表、默认规则等),不是 mock 测试数据。 文件仅用于首次部署时初始化,后续数据通过 API 接口维护。
- 文件命名:
V{版本号}__{模块描述}_init_data.sql - ID 命名:
{模块缩写}-{实体缩写}-{序号},如MR-RULE-001、CW-EVI-001 - 审计字段默认值:
create_time/update_time:TO_TIMESTAMP('2026-05-18 10:00:00', 'YYYY-MM-DD HH24:MI:SS')create_by/update_by:'system'create_name/update_name:'系统管理员'
- 数据原则:贴近真实业务场景,数量以覆盖主要业务分支为准
- 标记字段:
is_initial_data = 1标记初始数据,用户通过 API 创建的数据为0
D. 评估
每个模块开发完成后必须执行的验证步骤。
D1. 编译验证
# 1. 删除目标模块编译产物(确保强制重编译)
rm -rf target/classes/com/chinaweal/youfool/prj/modules/{module-name}/
# 2. 编译
JAVA_HOME="/d/Program Files/Java/jdk-21.0.11+10" \
/d/apache-maven-3.9.15/bin/mvn compile -Pcompany-nexus -Dcheckstyle.skip=true
# 3. 确认输出 BUILD SUCCESS
D2. 数据库验证
# 通过 Python + JayDeBeApi 连接后执行
cursor.execute("SELECT TABLE_NAME FROM ALL_TABLES WHERE OWNER='OARMS' AND TABLE_NAME LIKE '{PREFIX}%'")
# 确认表已创建
cursor.execute("SELECT COUNT(*) FROM OARMS.{TABLE_NAME}")
# 确认初始数据行数符合预期
D3. 模块完成检查清单
- DDL 文件已创建(
docs/db/sql/V*__*_ddl.sql) - DDL 已执行到 DM8 数据库,表和索引存在
- 初始数据文件已创建(
docs/db/sql/V*__*_init_data.sql,如需要) - 初始数据已插入数据库,行数正确
- Entity / Query / Req / VO 类已创建,继承 SuperEntity
- Mapper 接口已创建,继承 BaseMapper
- Service 接口 + ServiceImpl 已创建,事务用
@DSTransactional - Controller 已创建,API 路径和参数与前端 s4 文档一致
mvn compileBUILD SUCCESS,无编译错误(warnings 可忽略)- 启动应用,Knife4j 文档可访问(
/doc.html),新接口可见 - 通过 API 接口能正常读写数据库数据
E. 边界
必须遵守的约束和必须避免的错误。违反这些会导致编译失败或运行时异常。
E1. 数据库禁忌
| 禁忌 | 后果 | 正确做法 |
|---|---|---|
| COMMENT ON / CREATE INDEX 用小写表名 | 报"无效的表或视图" | 必须用大写:OARMS.BS_SCREEN |
DECIMAL(100,0) 等精度 > 38 |
DM8 精度溢出错误 | 改用 TINYINT 或 INT |
用 split(';') 解析 SQL 文件执行 |
注释或 CLOB 中的 ; 导致语句截断 |
按 SQL 语句逐条在 Python 中硬编码执行 |
连接串不带 ?schema=OARMS |
连接成功但操作错误的 schema | URL 必须包含 ?schema=OARMS |
用 OARMS / OARMS@2026 作为用户名密码 |
连接失败 | 用户名 SYSDBA,密码 chinaweal |
E2. 代码禁忌
| 禁忌 | 后果 | 正确做法 |
|---|---|---|
使用 @Transactional |
多数据源事务不生效 | 必须用 @DSTransactional |
实体类不加 @DS("master") |
默认走 youfool 数据源 | 所有业务实体加 @DS("master") |
实体类不加 schema = "OARMS" |
MyBatis-Plus 找不到表 | @TableName(schema = "OARMS", value = "表名") |
主键不加 @TableId |
MyBatis-Plus 无法识别主键 | @TableId(value = "id", type = IdType.ASSIGN_UUID) |
| SuperEntity 已有 id 字段 | 重复字段编译错误 | SuperEntity 不含 id,各实体自行定义 |
E3. 编译陷阱
| 问题 | 原因 | 解决方案 |
|---|---|---|
mvn clean compile 报 Failed to delete target/xxx.jar |
JAR 被运行中的 Java 进程锁定 | 先停应用,或用 mvn compile(跳过 clean) |
编译输出 Nothing to compile |
target 中已有旧 class | 先删除对应模块的 class 目录 |
| MapStruct unmapped warnings | SuperEntity 审计字段未映射 | 正常现象,不影响编译 |
| unchecked warnings | JSON 反序列化泛型操作 | 正常现象,不影响编译 |
F. Harness Engineering
从需求到上线的完整开发管线、模块进度和任务规划。
F1. 全流程
信息收集 → 设计建表 → DDL 脚本 → 初始数据 → Java 代码 → 执行建表 → 插入初始数据 → 编译验证 → 接口联调
Step 1: 信息收集(并行读取)
| 输入 | 路径 | 用途 |
|---|---|---|
| 需求文档 s1 | gz-oarms-web/docs/prd-llm/{域}*/{模块}*/s1-需求分析.md |
理解业务背景、角色、流程 |
| 实体定义 s2 | gz-oarms-web/docs/prd-llm/{域}*/{模块}*/s2-实体定义.md |
DDL 和 Entity 的主要来源 |
| API 设计 s4 | gz-oarms-web/docs/prd-llm/{域}*/{模块}*/s4-api-design.md |
Controller 接口定义 |
| 编码任务 s4 | gz-oarms-web/docs/prd-llm/{域}*/{模块}*/s4-编码任务.md |
页面和 API 总览 |
| 本开发指南 | docs/backend-module-dev-guide.md |
代码风格和流程规范 |
| 全局实体字典 | gz-oarms-web/docs/prd-llm/DM-全局数据模型/全局实体字典.md |
核心实体跨域引用 |
操作建议:并行读取 s1 + s2 + s4(api) + s4(编码任务),不要串行。
Step 2: 设计建表
根据 s2 实体定义,产出 DDL 脚本(参见 C 记忆部分的模板和类型映射)。
Step 3: 准备初始数据
系统运行必需的种子数据(码表、默认配置、示例业务数据),参见 C10 初始数据约定。 不是所有模块都需要初始数据文件——部分模块的表由 API 接口填充。
Step 4: 生成 Java 代码
按 C3 包结构模板,依次生成:
Entity → Query/Req/VO → Mapper → Service(接口+实现) → Controller
Step 5: 执行建表 + 插入初始数据
使用 B2 数据库执行工具,按 D1/D2 评估步骤验证。
Step 6: 编译验证
执行 D3 检查清单。
Step 7: 接口联调
启动应用,通过 Knife4j(/doc.html)或前端页面验证 API 接口能正常读写数据库。
F2. 模块开发进度总表
状态标记:✅ 已完成 | 🔨 部分完成 | ⏳ 待开发
大屏基础库域(BS)
| 模块 | 功能 | DDL | 初始数据 | Java 代码 | 编译 | 备注 |
|---|---|---|---|---|---|---|
| BS-1 大屏基础信息管理 | 大屏 CRUD + 地图 + 导出 | ✅ | ✅ 8条 | ✅ | ⏳ | 核心实体 SCREEN |
法律法规库域(LB)
| 模块 | 功能 | DDL | 初始数据 | Java 代码 | 编译 | 备注 |
|---|---|---|---|---|---|---|
| LB-1 法律法规管理 | 法律法规 CRUD | ✅ | ✅ 5条 | ✅ | ⏳ |
广告监控域(AM)
| 模块 | 功能 | DDL | 初始数据 | Java 代码 | 编译 | 备注 |
|---|---|---|---|---|---|---|
| AM-1 广告画面录屏设置管理 | 录屏配置 CRUD | ✅ | - | ✅ | ⏳ | |
| AM-2 广告画面随机录屏 | 录屏任务管理 | ✅ | - | ✅ | ⏳ | |
| AM-3 广告画面监控 | 监控审核记录 | ✅ | - | ✅ | ⏳ |
监测规则库域(MR)
| 模块 | 功能 | DDL | 初始数据 | Java 代码 | 编译 | 备注 |
|---|---|---|---|---|---|---|
| MR-1 监测规则管理 | 监测规则/法律条款 CRUD | ✅ | ✅ 3+3+3条 | ✅ | ⏳ | 核心实体 MONITORING_RULE |
内容预警域(CW)
| 模块 | 功能 | DDL | 初始数据 | Java 代码 | 编译 | 备注 |
|---|---|---|---|---|---|---|
| CW-1 广告监测固化取证 | 取证记录管理 | ✅ | - | ✅ | ⏳ | 核心实体 EVIDENCE_RECORD |
| CW-2 监测规则关联 | 证据-规则多对多 | ✅ | - | ✅ | ⏳ | |
| CW-3 监测线索生成 | 线索生成与管理 | ✅ | - | ✅ | ⏳ | |
| CW-4 对接线索转办 | 线索推送转办 | ✅ | - | ✅ | ⏳ |
F3. 核心实体跨域依赖
SCREEN (BS-1) ──── 被引用于 ──── AM-1/2/3(录屏配置/任务/监控)
CW-1(取证关联大屏)
MONITORING_RULE (MR-1) ──── 被引用于 ──── CW-2(证据规则关联)
CW-3(线索生成依据)
EVIDENCE_RECORD (CW-1) ──── 被引用于 ──── CW-2(规则关联)
CW-3(线索来源)
CW-4(转办依据)
开发建议:先开发 BS-1(大屏)和 MR-1(规则),这两个是核心实体,被多个下游模块依赖。
F4. 待开发任务建议顺序
P1: BS-1 大屏基础信息管理
- 需求文档:
BS-大屏基础库域/BS-1_大屏基础信息管理/ - 核心实体:SCREEN(评分 0.956,全系统最高)
- 被依赖:AM-1/2/3、CW-1 均关联大屏
- 工作量:中(标准 CRUD + 地图 + 导出)
P2: MR-1 监测规则管理
- 需求文档:
MR-监测规则库域/MR-1_监测规则管理/ - 核心实体:MONITORING_RULE(评分 0.738)
- 被依赖:CW-2(证据规则关联)、CW-3(线索生成)
- 工作量:中(规则 + 法律条款 CRUD)
P3: AM-1~3 广告监控(可并行)
- 需求文档:
AM-广告监控域/AM-{1,2,3}_*/ - 依赖:BS-1(关联大屏)
- 工作量:大(3 个子模块)
P4: CW-1~4 内容预警(顺序开发)
- 需求文档:
CW-内容预警域/CW-{1,2,3,4}_*/ - 依赖:BS-1(关联大屏)+ MR-1(关联规则)
- 工作量:大(4 个子模块,建议 CW-1 → CW-2 → CW-3 → CW-4 顺序)
P5: LB-1 法律法规管理
- 需求文档:
LB-法律法规库域/LB-1_法律法规管理/ - 依赖:无强依赖,但 MR-1 会引用法律条款
- 工作量:小(标准 CRUD)
- 建议:可与 P1/P2 并行开发