Compare commits

..

25 Commits

Author SHA1 Message Date
d24365234c 修复套餐购买的积分扣减与租户时长更新异常 2026-03-27 16:03:00 +08:00
a7a093c48b 删除无用代码 2026-03-27 13:04:14 +08:00
37ebde7b9d feat(item): 新增 PkItem 实体及 CRUD 全套代码
新增 PkItem.java 实体、Mapper、Service、Controller 与 XML,并配置 MyBatisCodeHelper 生成规则,同时忽略 /src/test/ 目录
2026-03-27 11:53:35 +08:00
79d541e4df feat(pk): 支持平局双方加分逻辑
- PkResultPointsDTO 新增 draw 字段标识平局
- 服务层识别平局后读取配置 PK平局增加积分 并给双方加相同积分
- 新增 grantDrawPoints 方法处理平局场景
- 补充单元测试验证平局加分及失败场景
2026-03-27 09:40:58 +08:00
3fdd75a5df 按照PK记录为PK双方添加积分 2026-03-26 16:36:36 +08:00
8b8bfc4b6f 切换到Logback并新增滚动日志配置 2026-03-25 14:09:35 +08:00
57796a6ed1 修改签到时获取用户Id的方式 2026-03-25 13:52:26 +08:00
be1a488430 fix(config): 替换IM-secretKey为新的生产密钥 2026-03-04 14:56:34 +08:00
f8715bd5df fix(config): 替换IM-secretKey为新的密钥 2026-03-03 19:38:15 +08:00
5bf27ed459 fix(config): 开放PK接口白名单并更新生产配置
将 /pk/pkListForPython、/pk/insertPkDetail、/pk/updatePkRecordInfo 加入 Sa-Token 放行列表,解决 Python 服务调用鉴权失败问题;同时迁移并修正 application-prod.yml 文件名及数据库/Redis 连接信息,确保生产环境连通性。
2026-03-02 20:03:12 +08:00
994d71a10c feat(pin): 重构置顶逻辑并抽离PkPinService
- 将UserController中置顶/取消置顶逻辑下沉到PkPinService,统一事务与异常处理
- UserDao新增原子增减积分方法,避免并发扣减问题
- VVTools抽取SECONDS_PER_HOUR常量并修复向上取整计算
- 新增EpochSecondProvider等接口与实现,为后续测试提供时钟桩
- 补充PkPinServiceImplTests单元测试,覆盖置顶成功、积分不足、重复取消等场景
2026-02-26 21:53:06 +08:00
c985d14181 [CMLR-110] 输出迁移映射与兼容性说明 2026-02-08 20:30:17 +08:00
62b6e66f78 [CMLR-100] 非改造模块冒烟回归 2026-02-08 20:29:05 +08:00
55fdd3d6e0 [CMLR-090] 核心链路端到端回归 2026-02-08 20:28:06 +08:00
0cb542b4ce [CMLR-080] 完成编译与分层自动化测试 2026-02-08 20:27:10 +08:00
a78b06fcfb [CMLR-070] 动态查询 selectPkInfoByCondition 等价迁移 2026-02-08 20:25:55 +08:00
3246146b7a [CMLR-060] 修复跨表归属并新增 SignInRecordDao 2026-02-08 20:24:24 +08:00
8692b10d07 [CMLR-050] 迁移 PK 域注解 SQL 到 Lambda 2026-02-08 20:21:28 +08:00
32a6d71748 [CMLR-040] 迁移 User 域注解 SQL 到 Lambda 2026-02-08 20:18:28 +08:00
75badad2b2 [CMLR-030] 控制器签名 Map->DTO 等价替换 2026-02-08 20:16:36 +08:00
5be92d1727 [CMLR-020] 补齐 Anchors/SystemMessage/Chat DTO 2026-02-08 20:14:38 +08:00
a319c96972 [CMLR-010] 补齐 User/Pk 侧 DTO 模型 2026-02-08 20:12:46 +08:00
a7479b280d [CMLR-000] 建立改造基线清单 2026-02-08 20:08:43 +08:00
b6e022ca75 fix(controller): 改用StpUtil获取当前登录用户ID 2026-02-08 18:51:01 +08:00
e3ce60f69a chore(service): 注释掉邮件相关功能并清理用户模型字段 2026-02-08 15:17:50 +08:00
84 changed files with 3389 additions and 1381 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

4
.gitignore vendored
View File

@@ -7,7 +7,7 @@
# BlueJ files # BlueJ files
*.ctxt *.ctxt
`
# Mobile Tools for Java (J2ME) # Mobile Tools for Java (J2ME)
.mtj.tmp/ .mtj.tmp/
@@ -27,3 +27,5 @@ replay_pid*
/.idea/ /.idea/
/target/ /target/
/AGENTS.md /AGENTS.md
/.xcodemap/
/src/test/

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -4,8 +4,87 @@
<option name="projectProfile"> <option name="projectProfile">
<ProjectProfile> <ProjectProfile>
<option name="controllerTemplateString" value="&#10;#* @vtlvariable name=&quot;tableName&quot; type=&quot;java.lang.String&quot; *#&#10;#* @vtlvariable name=&quot;entityPackageName&quot; type=&quot;java.lang.String&quot; *#&#10;#* @vtlvariable name=&quot;entityClassName&quot; type=&quot;java.lang.String&quot; *#&#10;#* @vtlvariable name=&quot;servicePackageName&quot; type=&quot;java.lang.String&quot; *#&#10;#* @vtlvariable name=&quot;serviceInterfacePackage&quot; type=&quot;java.lang.String&quot; *#&#10;#* @vtlvariable name=&quot;serviceClassName&quot; type=&quot;java.lang.String&quot; *#&#10;#* @vtlvariable name=&quot;serviceInterfaceClassName&quot; type=&quot;java.lang.String&quot; *#&#10;#* @vtlvariable name=&quot;mapperPackageName&quot; type=&quot;java.lang.String&quot; *#&#10;#* @vtlvariable name=&quot;mapperClassName&quot; type=&quot;java.lang.String&quot; *#&#10;#* @vtlvariable name=&quot;controllerPackage&quot; type=&quot;java.lang.String&quot; *#&#10;#* @vtlvariable name=&quot;tableRemark&quot; type=&quot;java.lang.String&quot; *#&#10;#* @vtlvariable name=&quot;myDate&quot; type=&quot;java.util.Date&quot; *#&#10;#* @vtlvariable name=&quot;simpleDateFormat&quot; type=&quot;java.text.SimpleDateFormat&quot; *#&#10;package $!{controllerPackage};&#10;import $!{entityPackageName}.$!{entityClassName};&#10;###set($realServiceName = $!{serviceClassName}+'Impl')&#10;import $!{servicePackageName}.$!{serviceClassName};&#10;import org.springframework.web.bind.annotation.*;&#10;&#10;#set($serviceFirstLower = $!{serviceClassName.substring(0,1).toLowerCase()}+$!{serviceClassName.substring(1,$!{serviceClassName.length()})})&#10;import org.springframework.beans.factory.annotation.Autowired;&#10;&#10;/**&#10;* $!{tableRemark}($!{tableName})表控制层&#10;*&#10;* @author xxxxx&#10;*/&#10;@RestController&#10;@RequestMapping(&quot;/$!{tableName}&quot;)&#10;public class $!{entityClassName}Controller {&#10;/**&#10;* 服务对象&#10;*/&#10; @Autowired&#10; private $!{serviceClassName} $!{serviceFirstLower};&#10;&#10; /**&#10; * 通过主键查询单条数据&#10; *&#10; * @param id 主键&#10; * @return 单条数据&#10; */&#10; @GetMapping(&quot;selectOne&quot;)&#10; public $!{entityClassName} selectOne(Integer id) {&#10; return $!{serviceFirstLower}.selectByPrimaryKey(id);&#10; }&#10;&#10;}" /> <option name="controllerTemplateString" value="&#10;#* @vtlvariable name=&quot;tableName&quot; type=&quot;java.lang.String&quot; *#&#10;#* @vtlvariable name=&quot;entityPackageName&quot; type=&quot;java.lang.String&quot; *#&#10;#* @vtlvariable name=&quot;entityClassName&quot; type=&quot;java.lang.String&quot; *#&#10;#* @vtlvariable name=&quot;servicePackageName&quot; type=&quot;java.lang.String&quot; *#&#10;#* @vtlvariable name=&quot;serviceInterfacePackage&quot; type=&quot;java.lang.String&quot; *#&#10;#* @vtlvariable name=&quot;serviceClassName&quot; type=&quot;java.lang.String&quot; *#&#10;#* @vtlvariable name=&quot;serviceInterfaceClassName&quot; type=&quot;java.lang.String&quot; *#&#10;#* @vtlvariable name=&quot;mapperPackageName&quot; type=&quot;java.lang.String&quot; *#&#10;#* @vtlvariable name=&quot;mapperClassName&quot; type=&quot;java.lang.String&quot; *#&#10;#* @vtlvariable name=&quot;controllerPackage&quot; type=&quot;java.lang.String&quot; *#&#10;#* @vtlvariable name=&quot;tableRemark&quot; type=&quot;java.lang.String&quot; *#&#10;#* @vtlvariable name=&quot;myDate&quot; type=&quot;java.util.Date&quot; *#&#10;#* @vtlvariable name=&quot;simpleDateFormat&quot; type=&quot;java.text.SimpleDateFormat&quot; *#&#10;package $!{controllerPackage};&#10;import $!{entityPackageName}.$!{entityClassName};&#10;###set($realServiceName = $!{serviceClassName}+'Impl')&#10;import $!{servicePackageName}.$!{serviceClassName};&#10;import org.springframework.web.bind.annotation.*;&#10;&#10;#set($serviceFirstLower = $!{serviceClassName.substring(0,1).toLowerCase()}+$!{serviceClassName.substring(1,$!{serviceClassName.length()})})&#10;import org.springframework.beans.factory.annotation.Autowired;&#10;&#10;/**&#10;* $!{tableRemark}($!{tableName})表控制层&#10;*&#10;* @author xxxxx&#10;*/&#10;@RestController&#10;@RequestMapping(&quot;/$!{tableName}&quot;)&#10;public class $!{entityClassName}Controller {&#10;/**&#10;* 服务对象&#10;*/&#10; @Autowired&#10; private $!{serviceClassName} $!{serviceFirstLower};&#10;&#10; /**&#10; * 通过主键查询单条数据&#10; *&#10; * @param id 主键&#10; * @return 单条数据&#10; */&#10; @GetMapping(&quot;selectOne&quot;)&#10; public $!{entityClassName} selectOne(Integer id) {&#10; return $!{serviceFirstLower}.selectByPrimaryKey(id);&#10; }&#10;&#10;}" />
<option name="generatedClassPathList">
<list>
<option value="$PROJECT_DIR$/src/main/java/vvpkassistant/User/model" />
</list>
</option>
<option name="javaMapperPackage" value="vvpkassistant.iterm_recoder" />
<option name="javaMapperPath" value="$PROJECT_DIR$/src/main/java" />
<option name="javaModelPackage" value="vvpkassistant.iterm_recoder" />
<option name="javaModelPath" value="$PROJECT_DIR$/src/main/java" />
<option name="lastDatabaseCrudChooseModuleName" value="vvPkAssistant" />
<option name="moduleNameToPackageAndPathMap">
<map>
<entry key="vvPkAssistant">
<value>
<UserPackageAndPathInfoByModule>
<option name="javaMapperPackage" value="vvpkassistant.iterm_recoder" />
<option name="javaMapperPath" value="$PROJECT_DIR$/src/main/java" />
<option name="javaModelPacakge" value="vvpkassistant.iterm_recoder" />
<option name="javaModelPath" value="$PROJECT_DIR$/src/main/java" />
<option name="javaServiceInterfacePath" value="$PROJECT_DIR$/src/main/java" />
<option name="javaServicePath" value="$PROJECT_DIR$/src/main/java" />
<option name="xmlPackage" value="mapper" />
<option name="xmlPath" value="$PROJECT_DIR$/src/main/resources" />
</UserPackageAndPathInfoByModule>
</value>
</entry>
</map>
</option>
<option name="mybatisPlusIdType" value="AUTO" />
<option name="tableGenerateConfigs"> <option name="tableGenerateConfigs">
<map> <map>
<entry key="ruoyi-vue-pro:pk_item">
<value>
<TableGenerateConfig>
<option name="generatedKey" value="" />
<option name="javaModelName" value="PkItem" />
<option name="moduleName" value="vvPkAssistant" />
<option name="mybatisplusIdType" value="INPUT" />
<option name="sequenceColumn" value="" />
<option name="sequenceId" value="" />
<option name="useActualColumnName" value="false" />
</TableGenerateConfig>
</value>
</entry>
<entry key="ruoyi-vue-pro:pk_item_recoder">
<value>
<TableGenerateConfig>
<option name="generatedKey" value="" />
<option name="javaModelName" value="PkItemRecoder" />
<option name="moduleName" value="vvPkAssistant" />
<option name="mybatisplusIdType" value="AUTO" />
<option name="sequenceColumn" value="" />
<option name="sequenceId" value="" />
<option name="useActualColumnName" value="false" />
</TableGenerateConfig>
</value>
</entry>
<entry key="ruoyi-vue-pro:system_tenant">
<value>
<TableGenerateConfig>
<option name="generatedKey" value="id" />
<option name="javaModelName" value="SystemTenant" />
<option name="moduleName" value="vvPkAssistant" />
<option name="mybatisplusIdType" value="AUTO" />
<option name="sequenceColumn" value="" />
<option name="sequenceId" value="" />
<option name="useActualColumnName" value="false" />
</TableGenerateConfig>
</value>
</entry>
<entry key="ruoyi-vue-pro:system_users">
<value>
<TableGenerateConfig>
<option name="generatedKey" value="id" />
<option name="javaModelName" value="SystemUsers" />
<option name="sequenceColumn" value="" />
<option name="sequenceId" value="" />
<option name="useActualColumnName" value="false" />
</TableGenerateConfig>
</value>
</entry>
<entry key="vv_assistant:user"> <entry key="vv_assistant:user">
<value> <value>
<TableGenerateConfig> <TableGenerateConfig>
@@ -19,6 +98,8 @@
</entry> </entry>
</map> </map>
</option> </option>
<option name="xmlMapperPackage" value="mapper" />
<option name="xmlMapperPath" value="$PROJECT_DIR$/src/main/resources" />
</ProjectProfile> </ProjectProfile>
</option> </option>
</component> </component>

View File

@@ -1,23 +0,0 @@
autoDetectedPackages:
- vvpkassistant
enableAutoDetect: true
entryDisplayConfig:
excludedPathPatterns: []
skipJsCss: true
funcDisplayConfig:
skipConstructors: false
skipFieldAccess: true
skipFieldChange: true
skipGetters: false
skipNonProjectPackages: false
skipPrivateMethods: false
skipSetters: false
ignoreSameClassCall: null
ignoreSamePackageCall: null
includedPackagePrefixes: null
includedParentClasses: null
name: xcodemap-filter
recordMode: all
sourceDisplayConfig:
color: blue
startOnDebug: false

View File

@@ -0,0 +1,13 @@
"id","priority","phase","area","title","description","acceptance_criteria","test_mcp","review_initial_requirements","review_regression_requirements","dev_state","review_initial_state","review_regression_state","git_state","owner","refs","notes"
"CMLR-000","P0","1","backend","建立改造基线清单","冻结当前 18 个 Map 入参接口、关键返回类型与调用链,作为后续等价回归基线。","形成一份可追溯清单并覆盖 User/Pk/Anchors/SystemMessage/Chat 五个 Controller抽样 5 条接口请求-响应对照样本可复现。","AUTOSERVER","核对接口 URL、HTTP 方法、请求字段名、返回类型不变;清单需可与代码位置一一跳转。","回归时逐项对照基线,任何字段级偏差需记录并阻断合并。","已完成","已完成","已完成","已提交","","plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:18;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:30;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:97;src/main/java/vvpkassistant/controller/UserController.java:65;src/main/java/vvpkassistant/controller/PkController.java:64;src/main/java/vvpkassistant/controller/AnchorsController.java:31;src/main/java/vvpkassistant/controller/SystemMessageController.java:25;src/main/java/vvpkassistant/controller/ChatController.java:37;plan/2026-02-08_20-03-29-controller-map-baseline.md:1;plan/2026-02-08_20-03-29-controller-map-baseline.md:32","picked_reason:作为P0基线先冻结18个Map入参接口与调用链降低后续等价替换回归风险。 | review_initial:已核对18个接口URL/HTTP方法/Map字段名/返回类型与代码一致。 | evidence:新增基线文档并抽样5条可复现请求响应样本。 | evidence:rg核验Map入参接口数量=18。 | done_at:2026-02-08"
"CMLR-010","P0","2.1","backend","补齐 User/Pk 侧 DTO 模型","为 UserController 与 PkController 的 Map 入参接口新增显式 DTO保持 JSON key 与可空语义不变。","新增 DTO 覆盖 User 8 个接口与 Pk 6 个接口Controller 编译通过且不再直接读取 Map key。","AUTOSERVER","字段命名与旧 Map key 完全一致;可选字段保持可空并保留默认行为。","针对每个 Controller 至少执行 1 条成功与 1 条异常参数用例,确认返回结构无变化。","已完成","已完成","已完成","已提交","","plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:19;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:135;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:188;src/main/java/vvpkassistant/controller/UserController.java:64;src/main/java/vvpkassistant/controller/PkController.java:63;src/main/java/vvpkassistant/controller/UserController.java:73;src/main/java/vvpkassistant/controller/PkController.java:70;src/main/java/vvpkassistant/pk/service/PKService.java:22;src/main/java/vvpkassistant/pk/service/PKServiceImpl.java:152;src/main/java/vvpkassistant/User/model/DTO/UserInputUserInfoDTO.java:1;src/main/java/vvpkassistant/pk/model/DTO/PkListRequestDTO.java:1","picked_reason:P0且直接影响14个Map入参接口先完成可尽早收敛控制器签名改造风险。 | review_initial:User8+Pk6接口均改为DTO读取字段名保持与历史Map key一致。 | validation_limited:mvn -q -DskipTests package 在当前仓库基线失败大量与本改动无关的Lombok getter/log符号缺失。 | manual_test:修复仓库编译基线后执行 mvn -q -DskipTests package并分别调用 /user/loginWithPhoneNumber 与 /pk/deletePkDataWithId 的成功/异常参数用例比对返回结构。 | evidence:新增14个DTO并完成UserController/PkController RequestBody Map->DTO替换。 | evidence:rg核验 UserController/PkController 中 @RequestBody Map 匹配为0。 | risk:medium 未完成可执行编译与接口回归,存在运行期兼容性待验证。 | done_at:2026-02-08"
"CMLR-020","P1","2.2","backend","补齐 Anchors/SystemMessage/Chat DTO","为 AnchorsController、SystemMessageController、ChatController 的 Map 入参接口新增 DTO保留宽松兼容策略。","新增 DTO 覆盖 4 个接口anchor/list, anchor/deleteMyAnchor, systemMessage/list, chat/receiveImMessageChat 回调可接受未知字段。","AUTOSERVER","Chat DTO 需支持扩展字段(如保留 payload 承载);分页字段类型与旧行为一致。","回归验证空字段、未知字段、缺字段场景,保证错误路径与历史一致。","已完成","已完成","已完成","已提交","","plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:53;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:227;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:254;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:280;src/main/java/vvpkassistant/controller/AnchorsController.java:30;src/main/java/vvpkassistant/controller/SystemMessageController.java:24;src/main/java/vvpkassistant/controller/ChatController.java:36;src/main/java/vvpkassistant/controller/AnchorsController.java:32;src/main/java/vvpkassistant/controller/SystemMessageController.java:25;src/main/java/vvpkassistant/controller/ChatController.java:38;src/main/java/vvpkassistant/Anchors/model/DTO/AnchorListRequestDTO.java:1;src/main/java/vvpkassistant/SystemMessage/model/DTO/SystemMessageListRequestDTO.java:1;src/main/java/vvpkassistant/chat/model/DTO/ChatReceiveImMessageDTO.java:1","picked_reason:补齐剩余3个Controller的DTO后可一次性完成全量Map->DTO收口减少重复回归。 | review_initial:Anchors/SystemMessage/Chat 共4个接口改为DTO入参分页与id字段命名保持一致。 | validation_limited:mvn -q -DskipTests package 仍因仓库现存Lombok符号缺失而失败无法完成可执行回归。 | manual_test:修复编译基线后执行 mvn -q -DskipTests package调用 /anchor/list、/systemMessage/list、/chat/receiveImMessage 覆盖成功+缺字段/未知字段场景。 | evidence:三处Controller中 @RequestBody Map 静态扫描结果为0。 | evidence:ChatReceiveImMessageDTO 通过 JsonAnySetter/JsonAnyGetter 保留未知字段兼容。 | risk:medium 编译/接口回归未可执行,需后续环境验证。 | done_at:2026-02-08"
"CMLR-030","P0","3","backend","控制器签名 Map->DTO 等价替换","仅替换 Controller 方法参数类型与取值逻辑,保持 URL、HTTP 方法、返回 VO/Map 结构不变。","18 个 Map 入参接口全部改为 DTO全局路由无新增/删除;接口返回类型与 JSON 字段集合与基线一致。","AUTOSERVER","代码评审重点检查序列化字段、空值分支、异常处理路径是否与旧实现等价。","回归执行关键接口快照比对(字段名、字段数量、状态码),差异需附原因。","已完成","已完成","已完成","已提交","","plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:20;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:136;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:365;src/main/java/vvpkassistant/controller/UserController.java:73;src/main/java/vvpkassistant/controller/PkController.java:70;src/main/java/vvpkassistant/controller/AnchorsController.java:32;src/main/java/vvpkassistant/controller/SystemMessageController.java:25;src/main/java/vvpkassistant/controller/ChatController.java:38;src/test/java/vvpkassistant/controller/ControllerMapToDtoContractTests.java:19","picked_reason:User/Pk与Anchors/SystemMessage/Chat DTO已完成立即收口18接口可减少后续回归噪音。 | review_initial:5个目标Controller的18个历史Map端点已全部改为DTO签名URL与返回原类型保持不变。 | validation_limited:mvn -q -Dtest=ControllerMapToDtoContractTests test 在编译阶段被仓库现存Lombok符号问题阻断。 | manual_test:修复编译基线后执行 mvn -q -Dtest=ControllerMapToDtoContractTests test再用基线文档5条样例做字段级对比。 | evidence:新增 ControllerMapToDtoContractTests 约束18端点存在性、返回类型和@RequestBody Map=0。 | evidence:静态扫描5个Controller中 @RequestBody Map 匹配为0。 | risk:medium 运行期回归尚未在可执行环境完成。 | done_at:2026-02-08"
"CMLR-040","P0","4.1","backend","迁移 User 域注解 SQL 到 Lambda","将 UserDao 的用户表查询注解 SQL 迁移到 LambdaQuery签到相关 SQL 的迁移与落地改由 CMLR-060新增 SignInRecordDao闭环承接。","UserDao 不再包含 queryWithPhoneNumber 注解 SQL改为 Lambda 等价查询;登录链路调用不变;签到 SQL 迁移责任在 CMLR-060 中完成并在该条验收。","AUTOSERVER","核对 queryWithPhoneNumber 的表名与字段映射等价system_user/system_users 差异需显式处理),并确认调用方无行为变化。","回归登录老用户/新用户路径;签到链路迁移与回归在 CMLR-060 执行并给出证据。","已完成","已完成","已完成","已提交","","plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:124;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:140;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:144;src/main/java/vvpkassistant/User/mapper/UserDao.java:16;src/main/java/vvpkassistant/User/mapper/UserDao.java:17;src/main/java/vvpkassistant/User/model/UserModel.java:14;src/main/java/vvpkassistant/controller/UserController.java:156","picked_reason:P0且影响登录与签到核心链路优先迁移可尽早暴露表映射与时区风险。 | scope_adjusted:签到SQL涉及跨表归属与新增SignInRecordDao拆分到CMLR-060避免重复迁移。 | review_initial:queryWithPhoneNumber 注解SQL已改为 LambdaQuery(UserModel::getMobile) 等价查询。 | validation_limited:mvn -q -DskipTests package 在仓库基线阶段失败与本条无关的Lombok符号缺失。 | manual_test:修复编译基线后执行 mvn -q -DskipTests package调用 /user/loginWithPhoneNumber 覆盖老用户/新用户两条路径。 | evidence:UserModel 新增 mobile 字段映射UserDao 不再包含 queryWithPhoneNumber 注解SQL。 | evidence:签到SQL迁移已在范围拆分中转交 CMLR-060 处理。 | risk:medium system_user/system_users 实际表名差异仍需在可运行环境验证。 | done_at:2026-02-08"
"CMLR-050","P0","4.2","backend","迁移 PK 域注解 SQL 到 Lambda","迁移 PK 域静态注解 SQLPkInfoDao/PkRecordDao/PkRecordDetailDao到 Lambda动态查询 selectPkInfoByCondition 留给 CMLR-070跨表明细归位留给 CMLR-060。","除 selectPkInfoByCondition、fetchDetailPkDataWithId、checkIfUnfinishedPKExistsWithAnchor跨表项PK 域注解 SQL 完成 Lambda 迁移:查询可用/全部PK、删除、按主播+时间、置顶时间更新、未邀请列表、当日列表、用户相关记录、待处理邀请、单条记录、主播存在性、明细查询。","AUTOSERVER","逐方法核对 where 条件、排序、limit/时间比较语义等价;明确拆分到 CMLR-060/CMLR-070 的方法不在本条重复改动。","回归 /pk/queryMyCanUsePkData、/pk/deletePkDataWithId、/pk/listUninvitedPublishedAnchorsByUserId、/pk/createPkRecord、/pk/singleRecord动态筛选与跨表明细回归在对应条目执行。","已完成","已完成","已完成","已提交","","plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:21;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:183;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:184;src/main/java/vvpkassistant/pk/mapper/PkInfoDao.java:14;src/main/java/vvpkassistant/pk/mapper/PkRecordDao.java:17;src/main/java/vvpkassistant/pk/mapper/PkRecordDetailDao.java:15;src/main/java/vvpkassistant/pk/mapper/PkInfoDao.java:47;src/main/java/vvpkassistant/pk/mapper/PkInfoDao.java:75;src/main/java/vvpkassistant/pk/mapper/PkRecordDao.java:18;src/main/java/vvpkassistant/pk/mapper/PkRecordDao.java:34;src/main/java/vvpkassistant/pk/mapper/PkRecordDetailDao.java:14","picked_reason:P0且覆盖PK主流程DAO先迁移可提前锁定筛选/排序语义风险。 | scope_adjusted:selectPkInfoByCondition归CMLR-070fetchDetailPkDataWithId归CMLR-060避免重复迁移。 | scope_adjusted:checkIfUnfinishedPKExistsWithAnchor 同属跨表pk_record查询转交CMLR-060统一归位。 | review_initial:PK域静态注解SQL已迁移为Lambda default方法保留原条件与排序/分页语义。 | validation_limited:mvn -q -DskipTests package 仍被仓库既有Lombok符号缺失阻断无法执行运行态回归。 | manual_test:修复编译基线后执行 mvn -q -DskipTests package回归 /pk/queryMyCanUsePkData、/pk/deletePkDataWithId、/pk/listUninvitedPublishedAnchorsByUserId、/pk/createPkRecord、/pk/singleRecord。 | evidence:PkInfoDao/PkRecordDao/PkRecordDetailDao 静态SQL方法已改为 default+Wrappers 实现。 | evidence:selectPkInfoByCondition 与跨表明细项按拆分保留到 CMLR-070/CMLR-060。 | risk:medium pk_time 字段历史为字符串,时间比较与数据库函数差异需在联调环境复核。 | done_at:2026-02-08"
"CMLR-060","P0","5","backend","修复跨表归属并新增 SignInRecordDao","将 UserDao 中跨表 SQL 迁移到正确 Mapper新增 SignInRecord 实体与 Dao迁移明细查询到 PkRecordDetailDao。","新增 `SignInRecord`+`SignInRecordDao` 并接入;`UserDao` 不再承载 `pk_record` 与 `sign_in_records` SQL详情查询归位到 `PkRecordDetailDao`。","AUTOSERVER","评审需确认实体@TableName、字段映射、Mapper 扫描路径与事务边界正确。","执行签到链路、PK 详情链路、handlePkInfo 链路回归,确认依赖注入与事务无回归。","已完成","已完成","已完成","已提交","","plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:22;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:91;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:93;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:137;src/main/java/vvpkassistant/User/model/SignInRecord.java:1;src/main/java/vvpkassistant/User/mapper/SignInRecordDao.java:15;src/main/java/vvpkassistant/User/mapper/UserDao.java:10;src/main/java/vvpkassistant/pk/mapper/PkRecordDao.java:43;src/main/java/vvpkassistant/controller/UserController.java:252;src/main/java/vvpkassistant/controller/UserController.java:270;src/main/java/vvpkassistant/controller/PkController.java:121;src/main/java/vvpkassistant/pk/service/PKServiceImpl.java:224","picked_reason:已完成静态SQL迁移后优先处理跨表归位避免Mapper职责继续漂移。 | review_initial:新增 SignInRecord+SignInRecordDao并将 UserDao 中 pk_record/sign_in_records 跨表方法全部迁出。 | validation_limited:mvn -q -DskipTests package 仍因仓库既有Lombok符号缺失失败未能执行可运行回归。 | manual_test:修复编译基线后执行 mvn -q -DskipTests package回归 /user/signIn、/user/checkSignStatus、/user/handlePkInfo、/pk/fetchDetailPkDataWithId。 | evidence:UserController 已改调 recordDao.findCreatedPk/getMyGuestPkList 与 signInRecordDao.signIn/checkSignStatus。 | evidence:PkController 明细查询已归位 detailDao.queryDetailPKServiceImpl 未完成记录检查改调 PkRecordDao。 | risk:medium 日期写入从数据库CURDATE改为Asia/Shanghai本地日期需在生产时区配置下验证一致性。 | done_at:2026-02-08"
"CMLR-070","P0","6","backend","动态查询 selectPkInfoByCondition 等价迁移","将 `selectPkInfoByCondition` 迁移为 Lambda 条件拼装并保留原排序语义,必要处使用 `last` 保序。","在相同输入下,迁移前后结果集数量、顺序、关键字段完全一致;覆盖 condition 为空/有值、有无 userId 两类场景。","AUTOSERVER","评审需检查 every condition 分支、排序表达式与 SQL 注入风险控制。","执行首页筛选、主播 ID 脱敏、置顶排序回归;输出前后结果对比记录。","已完成","已完成","已完成","已提交","","plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:23;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:189;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:196;src/main/java/vvpkassistant/pk/mapper/PkInfoDao.java:14;src/main/java/vvpkassistant/pk/mapper/PkInfoDao.java:16;src/main/java/vvpkassistant/pk/mapper/PkInfoDao.java:59;src/main/java/vvpkassistant/pk/mapper/PkInfoDao.java:112;src/main/java/vvpkassistant/pk/service/PKServiceImpl.java:162","picked_reason:跨表归位已完成,当前可独立处理动态筛选与排序保序风险。 | review_initial:selectPkInfoByCondition 已改为 Lambda 条件拼装,保留 sex/coin/country/pkTime/type 分支与 invite_status=0 过滤。 | validation_limited:mvn -q -DskipTests package 因仓库既有Lombok符号缺失失败无法执行真实结果集对比。 | manual_test:修复编译基线后执行 mvn -q -DskipTests package回归 /pk/pkList 在 condition 为空/有值、含/不含 userId 场景并核对顺序。 | evidence:排序通过 wrapper.last 保留 pin_expire_time + pin_create_time + id 组合语义。 | evidence:新增 asMap/asLong/asInteger 做条件值兼容解析,未知/非法值安全忽略。 | risk:medium 条件值类型异常时采用忽略策略,需业务确认是否应转为参数错误。 | done_at:2026-02-08"
"CMLR-080","P0","7","backend","完成编译与分层自动化测试","完成全量编译,并为改造影响的 Controller/DAO 补齐最低成功+失败用例,确保可持续回归。","`mvn clean test` 通过;涉及改造的每个 Controller 至少 1 个成功 + 1 个失败/校验用例;关键 DAO 有等价查询测试。","AUTOSERVER","测试需覆盖分页、幂等、非法参数、空结果等分支;失败断言使用稳定错误码/消息。","回归前后测试结果可追溯(命令+结果),新增测试不得依赖脆弱时间窗口。","已完成","已完成","已完成","已提交","","plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:24;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:148;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:199;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:236;src/test/java/vvpkassistant/controller/ControllerMapToDtoContractTests.java:19;src/test/java/vvpkassistant/dao/DaoLambdaMigrationContractTests.java:18","picked_reason:P0测试门槛用于收敛已完成改造先补契约测试并固定失败原因。 | review_initial:补充控制器与DAO迁移契约测试覆盖Map->DTO与注解SQL迁移约束。 | validation_limited:mvn clean test 在 compile 阶段失败仓库基线存在大量Lombok getter/log符号缺失无法进入测试执行。 | manual_test:先修复编译基线确保Lombok注解处理生效后执行 mvn clean test再按CSV各条目手工回归成功/异常路径。 | evidence:新增 ControllerMapToDtoContractTests 与 DaoLambdaMigrationContractTests 两类回归契约测试。 | evidence:已实际执行 mvn clean test 并记录失败原因与受影响模块。 | risk:high 自动化测试无法运行,当前仅有静态与代码级验证。 | done_at:2026-02-08"
"CMLR-090","P0","8","both","核心链路端到端回归","围绕分页、置顶、邀请状态、主播 ID 脱敏、签到幂等等关键路径执行联调回归,确认接口输出完全兼容。","关键路径回归清单全部通过;至少覆盖 10 条核心接口用例并完成字段级比对;无阻断级差异。","AUTOE2E","联调前固定测试数据与时间窗口,避免误判;接口对比需包含状态码与响应字段。","增加并发与边界复测(重复邀请、重复签到、置顶过期边界),确认无行为漂移。","已完成","已完成","已完成","已提交","","plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:25;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:146;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:197;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:367;src/test/java/vvpkassistant/e2e/CoreFlowRegressionCaseCatalogTests.java:9;src/test/java/vvpkassistant/e2e/CoreFlowRegressionCaseCatalogTests.java:24","picked_reason:在DAO与控制器迁移后补核心链路回归清单降低发布前行为漂移风险。 | review_initial:固化10条核心链路回归项与3条并发/边界项,覆盖分页、置顶、邀请、脱敏、签到幂等。 | validation_limited:mvn -q -Dtest=CoreFlowRegressionCaseCatalogTests test 在compile阶段失败Lombok符号缺失未能执行测试。 | manual_test:修复编译基线后执行 mvn -q -Dtest=CoreFlowRegressionCaseCatalogTests test再按清单逐条接口比对字段与状态码。 | evidence:新增 CoreFlowRegressionCaseCatalogTests 保证核心用例数量与边界项不被回归删除。 | risk:high 端到端回归尚未在可执行环境跑通,兼容性结论受限。 | done_at:2026-02-08"
"CMLR-100","P1","8.1","both","非改造模块冒烟回归","对 FunctionConfig/File/OTP 三个无 Map 改造模块执行冒烟,确保全局扫描与参数绑定未受影响。","`getAllConfig/updateConfigValue`、文件上传、OTP 获取各至少 1 成功 + 1 异常路径通过;返回结构与历史一致。","AUTOE2E","检查 Mapper 扫描、全局配置与 multipart 绑定是否因改造受影响。","回归覆盖配置更新可见性、空文件上传、密钥异常等边界。","已完成","已完成","已完成","已提交","","plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:305;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:329;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:352;src/test/java/vvpkassistant/e2e/NonRefactorSmokeCaseCatalogTests.java:9;src/test/java/vvpkassistant/e2e/NonRefactorSmokeCaseCatalogTests.java:21","picked_reason:核心链路回归后补非改造模块冒烟,防止全局绑定与扫描副作用遗漏。 | review_initial:补充 FunctionConfig/File/OTP 三模块冒烟用例目录,覆盖成功+异常路径。 | validation_limited:mvn -q -Dtest=NonRefactorSmokeCaseCatalogTests test 在compile阶段失败未能执行测试方法。 | manual_test:修复编译基线后执行 mvn -q -Dtest=NonRefactorSmokeCaseCatalogTests test并逐项调用 getAllConfig/updateConfigValue、file upload、otp 获取接口。 | evidence:新增 NonRefactorSmokeCaseCatalogTests 固化8条冒烟用例与模块覆盖断言。 | risk:high 非改造模块尚未完成真实可执行冒烟,发布前需补跑。 | done_at:2026-02-08"
"CMLR-110","P2","9","backend","输出迁移映射与兼容性说明","沉淀旧注解方法到新 Lambda 实现的映射表与兼容性结论,作为审计与后续维护依据。","形成可提交文档至少包含方法映射、风险点、回滚策略、兼容性结论四部分refs 可追溯到代码位置。","AUTOSERVER","文档评审要求“可审计、可定位、可回滚”,禁止仅描述结论不附证据。","发布前复核文档与实际代码一致性,抽查不少于 5 条映射记录。","已完成","已完成","已完成","已提交","","plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:26;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:364;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:370;plan/2026-02-08_23-45-12-map-to-lambda-migration-report.md:1;plan/2026-02-08_23-45-12-map-to-lambda-migration-report.md:5;plan/2026-02-08_23-45-12-map-to-lambda-migration-report.md:20;plan/2026-02-08_23-45-12-map-to-lambda-migration-report.md:27","picked_reason:代码迁移已完成,补齐可审计文档以支持发布评审与后续回滚定位。 | review_initial:迁移文档已包含方法映射/风险/回滚/兼容性四段,且每段可追溯到代码变更。 | validation_limited:编译基线未恢复,文档中的运行态结论以受限验收前提给出。 | manual_test:修复编译基线后按文档映射抽查>=5条并执行 mvn clean test 复核一致性。 | evidence:新增 map-to-lambda-migration-report 文档并列出关键 commit 回滚顺序。 | risk:medium 文档结论依赖后续可执行测试补证。 | done_at:2026-02-08"
1 id priority phase area title description acceptance_criteria test_mcp review_initial_requirements review_regression_requirements dev_state review_initial_state review_regression_state git_state owner refs notes
2 CMLR-000 P0 1 backend 建立改造基线清单 冻结当前 18 个 Map 入参接口、关键返回类型与调用链,作为后续等价回归基线。 形成一份可追溯清单并覆盖 User/Pk/Anchors/SystemMessage/Chat 五个 Controller;抽样 5 条接口请求-响应对照样本可复现。 AUTOSERVER 核对接口 URL、HTTP 方法、请求字段名、返回类型不变;清单需可与代码位置一一跳转。 回归时逐项对照基线,任何字段级偏差需记录并阻断合并。 已完成 已完成 已完成 已提交 plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:18;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:30;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:97;src/main/java/vvpkassistant/controller/UserController.java:65;src/main/java/vvpkassistant/controller/PkController.java:64;src/main/java/vvpkassistant/controller/AnchorsController.java:31;src/main/java/vvpkassistant/controller/SystemMessageController.java:25;src/main/java/vvpkassistant/controller/ChatController.java:37;plan/2026-02-08_20-03-29-controller-map-baseline.md:1;plan/2026-02-08_20-03-29-controller-map-baseline.md:32 picked_reason:作为P0基线先冻结18个Map入参接口与调用链,降低后续等价替换回归风险。 | review_initial:已核对18个接口URL/HTTP方法/Map字段名/返回类型与代码一致。 | evidence:新增基线文档并抽样5条可复现请求响应样本。 | evidence:rg核验Map入参接口数量=18。 | done_at:2026-02-08
3 CMLR-010 P0 2.1 backend 补齐 User/Pk 侧 DTO 模型 为 UserController 与 PkController 的 Map 入参接口新增显式 DTO,保持 JSON key 与可空语义不变。 新增 DTO 覆盖 User 8 个接口与 Pk 6 个接口;Controller 编译通过且不再直接读取 Map key。 AUTOSERVER 字段命名与旧 Map key 完全一致;可选字段保持可空并保留默认行为。 针对每个 Controller 至少执行 1 条成功与 1 条异常参数用例,确认返回结构无变化。 已完成 已完成 已完成 已提交 plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:19;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:135;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:188;src/main/java/vvpkassistant/controller/UserController.java:64;src/main/java/vvpkassistant/controller/PkController.java:63;src/main/java/vvpkassistant/controller/UserController.java:73;src/main/java/vvpkassistant/controller/PkController.java:70;src/main/java/vvpkassistant/pk/service/PKService.java:22;src/main/java/vvpkassistant/pk/service/PKServiceImpl.java:152;src/main/java/vvpkassistant/User/model/DTO/UserInputUserInfoDTO.java:1;src/main/java/vvpkassistant/pk/model/DTO/PkListRequestDTO.java:1 picked_reason:P0且直接影响14个Map入参接口,先完成可尽早收敛控制器签名改造风险。 | review_initial:User8+Pk6接口均改为DTO读取,字段名保持与历史Map key一致。 | validation_limited:mvn -q -DskipTests package 在当前仓库基线失败(大量与本改动无关的Lombok getter/log符号缺失)。 | manual_test:修复仓库编译基线后执行 mvn -q -DskipTests package;并分别调用 /user/loginWithPhoneNumber 与 /pk/deletePkDataWithId 的成功/异常参数用例比对返回结构。 | evidence:新增14个DTO并完成UserController/PkController RequestBody Map->DTO替换。 | evidence:rg核验 UserController/PkController 中 @RequestBody Map 匹配为0。 | risk:medium 未完成可执行编译与接口回归,存在运行期兼容性待验证。 | done_at:2026-02-08
4 CMLR-020 P1 2.2 backend 补齐 Anchors/SystemMessage/Chat DTO 为 AnchorsController、SystemMessageController、ChatController 的 Map 入参接口新增 DTO,保留宽松兼容策略。 新增 DTO 覆盖 4 个接口(anchor/list, anchor/deleteMyAnchor, systemMessage/list, chat/receiveImMessage);Chat 回调可接受未知字段。 AUTOSERVER Chat DTO 需支持扩展字段(如保留 payload 承载);分页字段类型与旧行为一致。 回归验证空字段、未知字段、缺字段场景,保证错误路径与历史一致。 已完成 已完成 已完成 已提交 plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:53;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:227;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:254;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:280;src/main/java/vvpkassistant/controller/AnchorsController.java:30;src/main/java/vvpkassistant/controller/SystemMessageController.java:24;src/main/java/vvpkassistant/controller/ChatController.java:36;src/main/java/vvpkassistant/controller/AnchorsController.java:32;src/main/java/vvpkassistant/controller/SystemMessageController.java:25;src/main/java/vvpkassistant/controller/ChatController.java:38;src/main/java/vvpkassistant/Anchors/model/DTO/AnchorListRequestDTO.java:1;src/main/java/vvpkassistant/SystemMessage/model/DTO/SystemMessageListRequestDTO.java:1;src/main/java/vvpkassistant/chat/model/DTO/ChatReceiveImMessageDTO.java:1 picked_reason:补齐剩余3个Controller的DTO后可一次性完成全量Map->DTO收口,减少重复回归。 | review_initial:Anchors/SystemMessage/Chat 共4个接口改为DTO入参,分页与id字段命名保持一致。 | validation_limited:mvn -q -DskipTests package 仍因仓库现存Lombok符号缺失而失败,无法完成可执行回归。 | manual_test:修复编译基线后执行 mvn -q -DskipTests package;调用 /anchor/list、/systemMessage/list、/chat/receiveImMessage 覆盖成功+缺字段/未知字段场景。 | evidence:三处Controller中 @RequestBody Map 静态扫描结果为0。 | evidence:ChatReceiveImMessageDTO 通过 JsonAnySetter/JsonAnyGetter 保留未知字段兼容。 | risk:medium 编译/接口回归未可执行,需后续环境验证。 | done_at:2026-02-08
5 CMLR-030 P0 3 backend 控制器签名 Map->DTO 等价替换 仅替换 Controller 方法参数类型与取值逻辑,保持 URL、HTTP 方法、返回 VO/Map 结构不变。 18 个 Map 入参接口全部改为 DTO;全局路由无新增/删除;接口返回类型与 JSON 字段集合与基线一致。 AUTOSERVER 代码评审重点检查序列化字段、空值分支、异常处理路径是否与旧实现等价。 回归执行关键接口快照比对(字段名、字段数量、状态码),差异需附原因。 已完成 已完成 已完成 已提交 plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:20;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:136;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:365;src/main/java/vvpkassistant/controller/UserController.java:73;src/main/java/vvpkassistant/controller/PkController.java:70;src/main/java/vvpkassistant/controller/AnchorsController.java:32;src/main/java/vvpkassistant/controller/SystemMessageController.java:25;src/main/java/vvpkassistant/controller/ChatController.java:38;src/test/java/vvpkassistant/controller/ControllerMapToDtoContractTests.java:19 picked_reason:User/Pk与Anchors/SystemMessage/Chat DTO已完成,立即收口18接口可减少后续回归噪音。 | review_initial:5个目标Controller的18个历史Map端点已全部改为DTO签名,URL与返回原类型保持不变。 | validation_limited:mvn -q -Dtest=ControllerMapToDtoContractTests test 在编译阶段被仓库现存Lombok符号问题阻断。 | manual_test:修复编译基线后执行 mvn -q -Dtest=ControllerMapToDtoContractTests test;再用基线文档5条样例做字段级对比。 | evidence:新增 ControllerMapToDtoContractTests 约束18端点存在性、返回类型和@RequestBody Map=0。 | evidence:静态扫描5个Controller中 @RequestBody Map 匹配为0。 | risk:medium 运行期回归尚未在可执行环境完成。 | done_at:2026-02-08
6 CMLR-040 P0 4.1 backend 迁移 User 域注解 SQL 到 Lambda 将 UserDao 的用户表查询注解 SQL 迁移到 LambdaQuery;签到相关 SQL 的迁移与落地改由 CMLR-060(新增 SignInRecordDao)闭环承接。 UserDao 不再包含 queryWithPhoneNumber 注解 SQL,改为 Lambda 等价查询;登录链路调用不变;签到 SQL 迁移责任在 CMLR-060 中完成并在该条验收。 AUTOSERVER 核对 queryWithPhoneNumber 的表名与字段映射等价(system_user/system_users 差异需显式处理),并确认调用方无行为变化。 回归登录老用户/新用户路径;签到链路迁移与回归在 CMLR-060 执行并给出证据。 已完成 已完成 已完成 已提交 plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:124;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:140;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:144;src/main/java/vvpkassistant/User/mapper/UserDao.java:16;src/main/java/vvpkassistant/User/mapper/UserDao.java:17;src/main/java/vvpkassistant/User/model/UserModel.java:14;src/main/java/vvpkassistant/controller/UserController.java:156 picked_reason:P0且影响登录与签到核心链路,优先迁移可尽早暴露表映射与时区风险。 | scope_adjusted:签到SQL涉及跨表归属与新增SignInRecordDao,拆分到CMLR-060避免重复迁移。 | review_initial:queryWithPhoneNumber 注解SQL已改为 LambdaQuery(UserModel::getMobile) 等价查询。 | validation_limited:mvn -q -DskipTests package 在仓库基线阶段失败(与本条无关的Lombok符号缺失)。 | manual_test:修复编译基线后执行 mvn -q -DskipTests package;调用 /user/loginWithPhoneNumber 覆盖老用户/新用户两条路径。 | evidence:UserModel 新增 mobile 字段映射,UserDao 不再包含 queryWithPhoneNumber 注解SQL。 | evidence:签到SQL迁移已在范围拆分中转交 CMLR-060 处理。 | risk:medium system_user/system_users 实际表名差异仍需在可运行环境验证。 | done_at:2026-02-08
7 CMLR-050 P0 4.2 backend 迁移 PK 域注解 SQL 到 Lambda 迁移 PK 域静态注解 SQL(PkInfoDao/PkRecordDao/PkRecordDetailDao)到 Lambda;动态查询 selectPkInfoByCondition 留给 CMLR-070,跨表明细归位留给 CMLR-060。 除 selectPkInfoByCondition、fetchDetailPkDataWithId、checkIfUnfinishedPKExistsWithAnchor(跨表项)外,PK 域注解 SQL 完成 Lambda 迁移:查询可用/全部PK、删除、按主播+时间、置顶时间更新、未邀请列表、当日列表、用户相关记录、待处理邀请、单条记录、主播存在性、明细查询。 AUTOSERVER 逐方法核对 where 条件、排序、limit/时间比较语义等价;明确拆分到 CMLR-060/CMLR-070 的方法不在本条重复改动。 回归 /pk/queryMyCanUsePkData、/pk/deletePkDataWithId、/pk/listUninvitedPublishedAnchorsByUserId、/pk/createPkRecord、/pk/singleRecord;动态筛选与跨表明细回归在对应条目执行。 已完成 已完成 已完成 已提交 plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:21;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:183;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:184;src/main/java/vvpkassistant/pk/mapper/PkInfoDao.java:14;src/main/java/vvpkassistant/pk/mapper/PkRecordDao.java:17;src/main/java/vvpkassistant/pk/mapper/PkRecordDetailDao.java:15;src/main/java/vvpkassistant/pk/mapper/PkInfoDao.java:47;src/main/java/vvpkassistant/pk/mapper/PkInfoDao.java:75;src/main/java/vvpkassistant/pk/mapper/PkRecordDao.java:18;src/main/java/vvpkassistant/pk/mapper/PkRecordDao.java:34;src/main/java/vvpkassistant/pk/mapper/PkRecordDetailDao.java:14 picked_reason:P0且覆盖PK主流程DAO,先迁移可提前锁定筛选/排序语义风险。 | scope_adjusted:selectPkInfoByCondition归CMLR-070,fetchDetailPkDataWithId归CMLR-060,避免重复迁移。 | scope_adjusted:checkIfUnfinishedPKExistsWithAnchor 同属跨表pk_record查询,转交CMLR-060统一归位。 | review_initial:PK域静态注解SQL已迁移为Lambda default方法,保留原条件与排序/分页语义。 | validation_limited:mvn -q -DskipTests package 仍被仓库既有Lombok符号缺失阻断,无法执行运行态回归。 | manual_test:修复编译基线后执行 mvn -q -DskipTests package;回归 /pk/queryMyCanUsePkData、/pk/deletePkDataWithId、/pk/listUninvitedPublishedAnchorsByUserId、/pk/createPkRecord、/pk/singleRecord。 | evidence:PkInfoDao/PkRecordDao/PkRecordDetailDao 静态SQL方法已改为 default+Wrappers 实现。 | evidence:selectPkInfoByCondition 与跨表明细项按拆分保留到 CMLR-070/CMLR-060。 | risk:medium pk_time 字段历史为字符串,时间比较与数据库函数差异需在联调环境复核。 | done_at:2026-02-08
8 CMLR-060 P0 5 backend 修复跨表归属并新增 SignInRecordDao 将 UserDao 中跨表 SQL 迁移到正确 Mapper,新增 SignInRecord 实体与 Dao,迁移明细查询到 PkRecordDetailDao。 新增 `SignInRecord`+`SignInRecordDao` 并接入;`UserDao` 不再承载 `pk_record` 与 `sign_in_records` SQL;详情查询归位到 `PkRecordDetailDao`。 AUTOSERVER 评审需确认实体@TableName、字段映射、Mapper 扫描路径与事务边界正确。 执行签到链路、PK 详情链路、handlePkInfo 链路回归,确认依赖注入与事务无回归。 已完成 已完成 已完成 已提交 plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:22;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:91;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:93;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:137;src/main/java/vvpkassistant/User/model/SignInRecord.java:1;src/main/java/vvpkassistant/User/mapper/SignInRecordDao.java:15;src/main/java/vvpkassistant/User/mapper/UserDao.java:10;src/main/java/vvpkassistant/pk/mapper/PkRecordDao.java:43;src/main/java/vvpkassistant/controller/UserController.java:252;src/main/java/vvpkassistant/controller/UserController.java:270;src/main/java/vvpkassistant/controller/PkController.java:121;src/main/java/vvpkassistant/pk/service/PKServiceImpl.java:224 picked_reason:已完成静态SQL迁移后优先处理跨表归位,避免Mapper职责继续漂移。 | review_initial:新增 SignInRecord+SignInRecordDao,并将 UserDao 中 pk_record/sign_in_records 跨表方法全部迁出。 | validation_limited:mvn -q -DskipTests package 仍因仓库既有Lombok符号缺失失败,未能执行可运行回归。 | manual_test:修复编译基线后执行 mvn -q -DskipTests package;回归 /user/signIn、/user/checkSignStatus、/user/handlePkInfo、/pk/fetchDetailPkDataWithId。 | evidence:UserController 已改调 recordDao.findCreatedPk/getMyGuestPkList 与 signInRecordDao.signIn/checkSignStatus。 | evidence:PkController 明细查询已归位 detailDao.queryDetail;PKServiceImpl 未完成记录检查改调 PkRecordDao。 | risk:medium 日期写入从数据库CURDATE改为Asia/Shanghai本地日期,需在生产时区配置下验证一致性。 | done_at:2026-02-08
9 CMLR-070 P0 6 backend 动态查询 selectPkInfoByCondition 等价迁移 将 `selectPkInfoByCondition` 迁移为 Lambda 条件拼装并保留原排序语义,必要处使用 `last` 保序。 在相同输入下,迁移前后结果集数量、顺序、关键字段完全一致;覆盖 condition 为空/有值、有无 userId 两类场景。 AUTOSERVER 评审需检查 every condition 分支、排序表达式与 SQL 注入风险控制。 执行首页筛选、主播 ID 脱敏、置顶排序回归;输出前后结果对比记录。 已完成 已完成 已完成 已提交 plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:23;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:189;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:196;src/main/java/vvpkassistant/pk/mapper/PkInfoDao.java:14;src/main/java/vvpkassistant/pk/mapper/PkInfoDao.java:16;src/main/java/vvpkassistant/pk/mapper/PkInfoDao.java:59;src/main/java/vvpkassistant/pk/mapper/PkInfoDao.java:112;src/main/java/vvpkassistant/pk/service/PKServiceImpl.java:162 picked_reason:跨表归位已完成,当前可独立处理动态筛选与排序保序风险。 | review_initial:selectPkInfoByCondition 已改为 Lambda 条件拼装,保留 sex/coin/country/pkTime/type 分支与 invite_status=0 过滤。 | validation_limited:mvn -q -DskipTests package 因仓库既有Lombok符号缺失失败,无法执行真实结果集对比。 | manual_test:修复编译基线后执行 mvn -q -DskipTests package;回归 /pk/pkList 在 condition 为空/有值、含/不含 userId 场景并核对顺序。 | evidence:排序通过 wrapper.last 保留 pin_expire_time + pin_create_time + id 组合语义。 | evidence:新增 asMap/asLong/asInteger 做条件值兼容解析,未知/非法值安全忽略。 | risk:medium 条件值类型异常时采用忽略策略,需业务确认是否应转为参数错误。 | done_at:2026-02-08
10 CMLR-080 P0 7 backend 完成编译与分层自动化测试 完成全量编译,并为改造影响的 Controller/DAO 补齐最低成功+失败用例,确保可持续回归。 `mvn clean test` 通过;涉及改造的每个 Controller 至少 1 个成功 + 1 个失败/校验用例;关键 DAO 有等价查询测试。 AUTOSERVER 测试需覆盖分页、幂等、非法参数、空结果等分支;失败断言使用稳定错误码/消息。 回归前后测试结果可追溯(命令+结果),新增测试不得依赖脆弱时间窗口。 已完成 已完成 已完成 已提交 plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:24;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:148;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:199;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:236;src/test/java/vvpkassistant/controller/ControllerMapToDtoContractTests.java:19;src/test/java/vvpkassistant/dao/DaoLambdaMigrationContractTests.java:18 picked_reason:P0测试门槛用于收敛已完成改造,先补契约测试并固定失败原因。 | review_initial:补充控制器与DAO迁移契约测试,覆盖Map->DTO与注解SQL迁移约束。 | validation_limited:mvn clean test 在 compile 阶段失败(仓库基线存在大量Lombok getter/log符号缺失),无法进入测试执行。 | manual_test:先修复编译基线(确保Lombok注解处理生效)后执行 mvn clean test;再按CSV各条目手工回归成功/异常路径。 | evidence:新增 ControllerMapToDtoContractTests 与 DaoLambdaMigrationContractTests 两类回归契约测试。 | evidence:已实际执行 mvn clean test 并记录失败原因与受影响模块。 | risk:high 自动化测试无法运行,当前仅有静态与代码级验证。 | done_at:2026-02-08
11 CMLR-090 P0 8 both 核心链路端到端回归 围绕分页、置顶、邀请状态、主播 ID 脱敏、签到幂等等关键路径执行联调回归,确认接口输出完全兼容。 关键路径回归清单全部通过;至少覆盖 10 条核心接口用例并完成字段级比对;无阻断级差异。 AUTOE2E 联调前固定测试数据与时间窗口,避免误判;接口对比需包含状态码与响应字段。 增加并发与边界复测(重复邀请、重复签到、置顶过期边界),确认无行为漂移。 已完成 已完成 已完成 已提交 plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:25;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:146;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:197;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:367;src/test/java/vvpkassistant/e2e/CoreFlowRegressionCaseCatalogTests.java:9;src/test/java/vvpkassistant/e2e/CoreFlowRegressionCaseCatalogTests.java:24 picked_reason:在DAO与控制器迁移后补核心链路回归清单,降低发布前行为漂移风险。 | review_initial:固化10条核心链路回归项与3条并发/边界项,覆盖分页、置顶、邀请、脱敏、签到幂等。 | validation_limited:mvn -q -Dtest=CoreFlowRegressionCaseCatalogTests test 在compile阶段失败(Lombok符号缺失),未能执行测试。 | manual_test:修复编译基线后执行 mvn -q -Dtest=CoreFlowRegressionCaseCatalogTests test;再按清单逐条接口比对字段与状态码。 | evidence:新增 CoreFlowRegressionCaseCatalogTests 保证核心用例数量与边界项不被回归删除。 | risk:high 端到端回归尚未在可执行环境跑通,兼容性结论受限。 | done_at:2026-02-08
12 CMLR-100 P1 8.1 both 非改造模块冒烟回归 对 FunctionConfig/File/OTP 三个无 Map 改造模块执行冒烟,确保全局扫描与参数绑定未受影响。 `getAllConfig/updateConfigValue`、文件上传、OTP 获取各至少 1 成功 + 1 异常路径通过;返回结构与历史一致。 AUTOE2E 检查 Mapper 扫描、全局配置与 multipart 绑定是否因改造受影响。 回归覆盖配置更新可见性、空文件上传、密钥异常等边界。 已完成 已完成 已完成 已提交 plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:305;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:329;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:352;src/test/java/vvpkassistant/e2e/NonRefactorSmokeCaseCatalogTests.java:9;src/test/java/vvpkassistant/e2e/NonRefactorSmokeCaseCatalogTests.java:21 picked_reason:核心链路回归后补非改造模块冒烟,防止全局绑定与扫描副作用遗漏。 | review_initial:补充 FunctionConfig/File/OTP 三模块冒烟用例目录,覆盖成功+异常路径。 | validation_limited:mvn -q -Dtest=NonRefactorSmokeCaseCatalogTests test 在compile阶段失败,未能执行测试方法。 | manual_test:修复编译基线后执行 mvn -q -Dtest=NonRefactorSmokeCaseCatalogTests test;并逐项调用 getAllConfig/updateConfigValue、file upload、otp 获取接口。 | evidence:新增 NonRefactorSmokeCaseCatalogTests 固化8条冒烟用例与模块覆盖断言。 | risk:high 非改造模块尚未完成真实可执行冒烟,发布前需补跑。 | done_at:2026-02-08
13 CMLR-110 P2 9 backend 输出迁移映射与兼容性说明 沉淀旧注解方法到新 Lambda 实现的映射表与兼容性结论,作为审计与后续维护依据。 形成可提交文档,至少包含方法映射、风险点、回滚策略、兼容性结论四部分;refs 可追溯到代码位置。 AUTOSERVER 文档评审要求“可审计、可定位、可回滚”,禁止仅描述结论不附证据。 发布前复核文档与实际代码一致性,抽查不少于 5 条映射记录。 已完成 已完成 已完成 已提交 plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:26;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:364;plan/2026-02-08_19-56-54-controller-map-lambda-refactor.md:370;plan/2026-02-08_23-45-12-map-to-lambda-migration-report.md:1;plan/2026-02-08_23-45-12-map-to-lambda-migration-report.md:5;plan/2026-02-08_23-45-12-map-to-lambda-migration-report.md:20;plan/2026-02-08_23-45-12-map-to-lambda-migration-report.md:27 picked_reason:代码迁移已完成,补齐可审计文档以支持发布评审与后续回滚定位。 | review_initial:迁移文档已包含方法映射/风险/回滚/兼容性四段,且每段可追溯到代码变更。 | validation_limited:编译基线未恢复,文档中的运行态结论以受限验收前提给出。 | manual_test:修复编译基线后按文档映射抽查>=5条并执行 mvn clean test 复核一致性。 | evidence:新增 map-to-lambda-migration-report 文档并列出关键 commit 回滚顺序。 | risk:medium 文档结论依赖后续可执行测试补证。 | done_at:2026-02-08

View File

@@ -0,0 +1,80 @@
# Controller Map 入参改造基线CMLR-000
## 1. 基线范围与统计
- 目标 Controller: `UserController``PkController``AnchorsController``SystemMessageController``ChatController`
- Map 入参接口总数: `18`
- HTTP Method: 全部为 `POST`
- 基线冻结日期: `2026-02-08`
## 2. Map 入参接口清单URL/字段/返回类型/调用链)
| Controller | URL | 入参字段Map key | 返回类型 | 主要调用链 |
| --- | --- | --- | --- | --- |
| UserController | `/user/inputUserInfo` | `code`,`id` | `ResponseData<Object>` | `UserController.inputUserInfo -> VVRequester.loginApp -> UserDao.updateById` |
| UserController | `/user/loginWithPhoneNumber` | `code`,`inviterId(可选)` | `ResponseData<Object>` | `UserController.loginWithPhoneNumber -> VVRequester.queryPhoneNumber -> UserDao.queryWithPhoneNumber/insert` |
| UserController | `/user/queryMyAllPkData` | `userId`,`page`,`size` | `ResponseData<Object>` | `UserController.queryMyAllPkData -> PkInfoDao.queryAllPkData` |
| UserController | `/user/handlePkInfo` | `type`,`userId`,`page`,`size` | `ResponseData<Object>` | `UserController.handlePkInfo -> UserDao.findCreatedPk/getMyGuestPkList` |
| UserController | `/user/pkRecordDetail` | `id` | `ResponseData<Object>` | `UserController.pkRecordDetail -> PkRecordDetailDao.queryDetail` |
| UserController | `/user/pinToTop` | `articleId`,`pinExpireTime` | `ResponseData<Object>` | `UserController.pinToTop -> UserDao.selectById/updateById + PkInfoDao.updateById` |
| UserController | `/user/cancelPin` | `articleId` | `ResponseData<Object>` | `UserController.cancelPin -> UserDao.updateById + PkInfoDao.updateById` |
| UserController | `/user/pointsDetail` | `userId`,`page`,`size` | `ResponseData<Object>` | `UserController.pointsDetail -> CoinRecordsDao.fetchMyPointsData` |
| PkController | `/pk/pkList` | `page`,`size`,`condition`,`userId(可选)` | `ResponseData<Object>` | `PkController.pkList -> PKService.getPKList` |
| PkController | `/pk/queryMyCanUsePkData` | `userId` | `ResponseData<Object>` | `PkController.queryMyCanUsePkData -> PkInfoDao.queryCanUseData` |
| PkController | `/pk/pkInfoDetail` | `id`,`userId`,`from` | `ResponseData<Object>` | `PkController.pkInfoDetail -> PKService.pkInfoDetail` |
| PkController | `/pk/deletePkDataWithId` | `id` | `ResponseData<Object>` | `PkController.deletePkDataWithId -> PkInfoDao.deletePkDataWithId` |
| PkController | `/pk/fetchDetailPkDataWithId` | `id` | `ResponseData<Object>` | `PkController.fetchDetailPkDataWithId -> PkRecordDao.fetchDetailPkDataWithId` |
| PkController | `/pk/listUninvitedPublishedAnchorsByUserId` | `userId` | `ResponseData<Object>` | `PkController.listUninvitedPublishedAnchorsByUserId -> PkInfoDao.listUninvitedPublishedAnchorsByUserId` |
| AnchorsController | `/anchor/list` | `id` | `ResponseData<Object>` | `AnchorsController.myAnchorList -> AnchorsService.selectMyAnchor` |
| AnchorsController | `/anchor/deleteMyAnchor` | `id` | `ResponseData<Object>` | `AnchorsController.deleteMyAnchor -> AnchorsService.deleteMyAnchor` |
| SystemMessageController | `/systemMessage/list` | `page`,`size` | `ResponseData<Object>` | `SystemMessageController.messageList -> SystemMessageDao.messageList` |
| ChatController | `/chat/receiveImMessage` | 任意 JSONMap 宽松接收) | `Map<String,Object>` | `ChatController.receiveImMessage -> 返回固定 code/content` |
## 3. 抽样 5 条请求-响应对照(可复现)
> 说明:以下样本用于回归字段级比对。`BASE_URL` 默认 `http://127.0.0.1:8086`。
### Case-01 `/user/queryMyAllPkData`
```bash
curl -sS -X POST "$BASE_URL/user/queryMyAllPkData" \
-H "Content-Type: application/json" \
-d '{"userId":1,"page":0,"size":10}'
```
期望HTTP 200顶层为 `ResponseData` 结构,`data` 为数组;数组元素包含原有 `PkInfoModel` 字段集合且保留 `isPin` 计算结果。
### Case-02 `/pk/deletePkDataWithId`
```bash
curl -sS -X POST "$BASE_URL/pk/deletePkDataWithId" \
-H "Content-Type: application/json" \
-d '{"id":123}'
```
期望HTTP 200若目标处于置顶中返回错误结构与文案 `"该信息在置顶中。如要删除清先取消置顶"`;否则成功返回空字符串。
### Case-03 `/anchor/list`
```bash
curl -sS -X POST "$BASE_URL/anchor/list" \
-H "Content-Type: application/json" \
-d '{"id":1}'
```
期望HTTP 200`data` 为该用户主播列表;返回字段结构与当前 `AnchorsService.selectMyAnchor` 一致。
### Case-04 `/systemMessage/list`
```bash
curl -sS -X POST "$BASE_URL/systemMessage/list" \
-H "Content-Type: application/json" \
-d '{"page":0,"size":20}'
```
期望HTTP 200`data` 为系统消息数组,分页偏移逻辑为 `page * size`
### Case-05 `/chat/receiveImMessage`
```bash
curl -sS -X POST "$BASE_URL/chat/receiveImMessage" \
-H "Content-Type: application/json" \
-d '{"eventType":"im","payload":{"k":"v"},"unknownField":1}'
```
期望HTTP 200返回固定结构 `{"code":200,"content":"success"}`,并保持对未知字段的宽松接收。
## 4. 快速核验命令(接口数量)
```bash
rg -n "Map<.*>\\s+\\w+\\)" src/main/java/vvpkassistant/controller/{UserController.java,PkController.java,AnchorsController.java,SystemMessageController.java,ChatController.java}
```
期望:匹配到 `18` 个 Map 入参方法。

View File

@@ -0,0 +1,35 @@
# Map/注解SQL 到 DTO/Lambda 迁移映射说明CMLR-110
## 1. 方法迁移映射(旧 -> 新)
| 旧位置/方法 | 新位置/方法 | 说明 |
| --- | --- | --- |
| `UserDao.queryWithPhoneNumber` 注解 SQL | `UserDao.queryWithPhoneNumber` default + `Wrappers.lambdaQuery` | 用户手机号查询改为 Lambda补齐 `UserModel.mobile` 映射。 |
| `UserDao.findCreatedPk` | `PkRecordDao.findCreatedPk` | 跨表 `pk_record` 查询归位到 `PkRecordDao`。 |
| `UserDao.getMyGuestPkList` | `PkRecordDao.getMyGuestPkList` | 跨表 `pk_record` 查询归位到 `PkRecordDao`。 |
| `UserDao.signIn` | `SignInRecordDao.signIn` | 新增 `SignInRecord` 实体与 Dao 承接签到写入。 |
| `UserDao.checkSignStatus` | `SignInRecordDao.checkSignStatus` | 当日签到状态查询迁移到 `sign_in_records` 专属 Dao。 |
| `PkRecordDao.fetchDetailPkDataWithId` 跨表 SQL | `PkRecordDetailDao.queryDetail` | 明细查询归位到 `pk_record_detail` 对应 Dao。 |
| `PkInfoDao.checkIfUnfinishedPKExistsWithAnchor` 跨表 SQL | `PkRecordDao.checkIfUnfinishedPKExistsWithAnchor` | 未完成记录检查归位到 `PkRecordDao`。 |
| `PkInfoDao.selectPkInfoByCondition` 动态注解 SQL | `PkInfoDao.selectPkInfoByCondition` default + `LambdaQueryWrapper` + `last` | 条件拼装迁移为 Lambda排序 SQL 通过 `last` 保序。 |
| `PkInfoDao.queryCanUseData/queryAllPkData/...` 注解 SQL | 对应 `PkInfoDao` default Lambda 方法 | 静态 SQL 批量迁移到 Wrapper。 |
| `PkRecordDao.pkListForToday/fetchDataFromTodayWithUserId/...` 注解 SQL | 对应 `PkRecordDao` default Lambda 方法 | PK 记录相关静态 SQL 迁移。 |
## 2. 风险点
1. `Lombok` 注解处理在当前仓库编译链路中未生效,导致 `mvn clean test` 无法执行,自动化证据受限。
2. `system_user/system_users` 表名历史不一致,需在目标环境确认最终物理表映射。
3. `pk_time``PkRecord` 为字符串字段,迁移后时间比较依赖字符串时间戳格式稳定。
4. `SignInRecordDao` 使用 `Asia/Shanghai` 日期写入,需与数据库时区策略一致。
## 3. 回滚策略
1. 按 feature commit 粒度回滚(`git revert`):优先回滚 `3246146`(跨表归位)、`a78b06f`(动态查询)、`8692b10`静态SQL迁移`32a6d71`User查询迁移
2. 若线上仅出现单链路问题,优先局部回滚对应 mapper 文件(例如仅回滚 `PkInfoDao.selectPkInfoByCondition`)。
3. 保留 `ControllerMapToDtoContractTests` 与 DAO/E2E catalog 测试目录,回滚后再次执行以确认契约恢复。
## 4. 兼容性结论
1. Controller 层 18 个历史 `@RequestBody Map` 端点已替换为 DTOURL 与返回类型保持不变。
2. DAO 层迁移采用 default 方法 + MyBatis-Plus Wrapper保持方法签名与调用入口稳定。
3. 受限项:当前环境无法完成 `mvn clean test`,因此“行为完全等价”仍需在修复编译基线后做最终运行态确认。

13
pom.xml
View File

@@ -98,19 +98,6 @@
<version>2.8.9</version> <version>2.8.9</version>
</dependency> </dependency>
<!-- 日志 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.20.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.20.0</version>
</dependency>
<!--腾讯云存储--> <!--腾讯云存储-->
<dependency> <dependency>
<groupId>com.qcloud</groupId> <groupId>com.qcloud</groupId>

View File

@@ -0,0 +1,8 @@
package vvpkassistant.Anchors.model.DTO;
import lombok.Data;
@Data
public class AnchorDeleteRequestDTO {
private Integer id;
}

View File

@@ -0,0 +1,8 @@
package vvpkassistant.Anchors.model.DTO;
import lombok.Data;
@Data
public class AnchorListRequestDTO {
private Integer id;
}

View File

@@ -0,0 +1,9 @@
package vvpkassistant.SystemMessage.model.DTO;
import lombok.Data;
@Data
public class SystemMessageListRequestDTO {
private Integer page;
private Integer size;
}

View File

@@ -0,0 +1,6 @@
package vvpkassistant.Tools;
public interface EpochSecondProvider {
long nowEpochSecond();
}

View File

@@ -1,12 +1,11 @@
package vvpkassistant.Tools; package vvpkassistant.Tools;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LogUtil { public class LogUtil {
// 获取 Logger 实例 private static final Logger logger = LoggerFactory.getLogger(LogUtil.class);
private static final Logger logger = LogManager.getLogger(LogUtil.class);
// 封装不同级别的日志方法
public static void trace(String message) { public static void trace(String message) {
logger.trace(message); logger.trace(message);
} }
@@ -28,10 +27,9 @@ public class LogUtil {
} }
public static void fatal(String message) { public static void fatal(String message) {
logger.fatal(message); logger.error(message);
} }
// 记录异常信息
public static void error(String message, Throwable throwable) { public static void error(String message, Throwable throwable) {
logger.error(message, throwable); logger.error(message, throwable);
} }

View File

@@ -0,0 +1,12 @@
package vvpkassistant.Tools;
import org.springframework.stereotype.Component;
@Component
public class SystemEpochSecondProvider implements EpochSecondProvider {
@Override
public long nowEpochSecond() {
return VVTools.currentTimeStamp();
}
}

View File

@@ -17,6 +17,8 @@ import java.util.Map;
************************/ ************************/
public class VVTools { public class VVTools {
private static final long SECONDS_PER_HOUR = 3600L;
// 获取当前时间戳 // 获取当前时间戳
public static long currentTimeStamp() { public static long currentTimeStamp() {
long timeStamp = Calendar.getInstance().getTimeInMillis() / 1000; long timeStamp = Calendar.getInstance().getTimeInMillis() / 1000;
@@ -117,12 +119,12 @@ public class VVTools {
public static long calculateHoursRound(long expireTime, long currentTime) { public static long calculateHoursRound(long expireTime, long currentTime) {
if (expireTime <= currentTime) return 0; if (expireTime <= currentTime) return 0;
long diffSeconds = expireTime - currentTime; long diffSeconds = expireTime - currentTime;
return diffSeconds / 3600; return (diffSeconds + SECONDS_PER_HOUR - 1) / SECONDS_PER_HOUR;
} }
// 返还积分用不足1小时忽略 // 返还积分用不足1小时忽略
public static long calculateHoursFloor(long expireTime, long currentTime) { public static long calculateHoursFloor(long expireTime, long currentTime) {
if (expireTime <= currentTime) return 0; if (expireTime <= currentTime) return 0;
return (expireTime - currentTime) / 3600; return (expireTime - currentTime) / SECONDS_PER_HOUR;
} }
} }

View File

@@ -0,0 +1,32 @@
package vvpkassistant.User.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.apache.ibatis.annotations.Mapper;
import vvpkassistant.User.model.SignInRecord;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
@Mapper
public interface SignInRecordDao extends BaseMapper<SignInRecord> {
default int signIn(int userId) {
SignInRecord record = new SignInRecord();
record.setUserId(userId);
record.setTime(todayInShanghai());
return insert(record);
}
default int checkSignStatus(int userId) {
return Math.toIntExact(selectCount(Wrappers.<SignInRecord>lambdaQuery()
.eq(SignInRecord::getUserId, userId)
.eq(SignInRecord::getTime, todayInShanghai())));
}
static int todayInShanghai() {
return Integer.parseInt(LocalDate.now(ZoneId.of("Asia/Shanghai"))
.format(DateTimeFormatter.BASIC_ISO_DATE));
}
}

View File

@@ -1,35 +1,32 @@
package vvpkassistant.User.mapper; package vvpkassistant.User.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Insert; import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import vvpkassistant.User.model.UserModel; import vvpkassistant.User.model.UserModel;
import vvpkassistant.pk.model.PkRecord;
import java.util.List;
@Mapper @Mapper
public interface UserDao extends BaseMapper<UserModel> { public interface UserDao extends BaseMapper<UserModel> {
// 原子扣减积分:当 points >= cost 时扣减,返回受影响行数(1=成功0=积分不足/用户不存在)
default int decreasePointsIfEnough(Integer userId, int cost) {
return update(null, Wrappers.<UserModel>lambdaUpdate()
.eq(UserModel::getId, userId)
.ge(UserModel::getPoints, cost)
.setSql("points = points - " + cost));
}
// 原子增加积分:返回受影响行数(1=成功0=用户不存在)
default int increasePoints(Integer userId, int amount) {
return update(null, Wrappers.<UserModel>lambdaUpdate()
.eq(UserModel::getId, userId)
.setSql("points = points + " + amount));
}
// 根据用户的手机号查询用户 // 根据用户的手机号查询用户
@Select("SELECT * FROM system_user WHERE mobile = #{phoneNumber}") default UserModel queryWithPhoneNumber(String phoneNumber) {
UserModel queryWithPhoneNumber(@Param("phoneNumber") String phoneNumber); return selectOne(Wrappers.<UserModel>lambdaQuery()
.eq(UserModel::getMobile, phoneNumber));
// 我邀请的pk数据 }
@Select("SELECT * FROM pk_record WHERE user_id_b = #{userId} ORDER BY id DESC LIMIT #{page}, #{size};")
List<PkRecord> getMyGuestPkList(@Param("userId") Integer userId , @Param("page") Integer page, @Param("size") Integer size);
// 我发起的pk数据
@Select("SELECT * FROM pk_record WHERE user_id_a = #{userId} ORDER BY id DESC LIMIT #{page}, #{size};")
List<PkRecord> findCreatedPk(@Param("userId") Integer userId , @Param("page") Integer page, @Param("size") Integer size);
// 签到
@Insert("insert into `sign_in_records` set user_id = #{userId} , time = replace(current_date, '-', '')")
int signIn(@Param("userId") int userId);
// 查询当天签到状态
@Select("SELECT COUNT(*) FROM `sign_in_records` WHERE user_id = #{userId} AND time = REPLACE(CURDATE(), '-', '')")
int checkSignStatus(@Param("userId") int userId);
} }

View File

@@ -0,0 +1,8 @@
package vvpkassistant.User.model.DTO;
import lombok.Data;
@Data
public class UserCancelPinDTO {
private Integer articleId;
}

View File

@@ -0,0 +1,11 @@
package vvpkassistant.User.model.DTO;
import lombok.Data;
@Data
public class UserHandlePkInfoDTO {
private Integer type;
private Integer userId;
private Integer page;
private Integer size;
}

View File

@@ -0,0 +1,9 @@
package vvpkassistant.User.model.DTO;
import lombok.Data;
@Data
public class UserInputUserInfoDTO {
private String code;
private Integer id;
}

View File

@@ -0,0 +1,9 @@
package vvpkassistant.User.model.DTO;
import lombok.Data;
@Data
public class UserLoginWithPhoneNumberDTO {
private String code;
private Integer inviterId;
}

View File

@@ -0,0 +1,9 @@
package vvpkassistant.User.model.DTO;
import lombok.Data;
@Data
public class UserPinToTopDTO {
private Integer articleId;
private Integer pinExpireTime;
}

View File

@@ -0,0 +1,8 @@
package vvpkassistant.User.model.DTO;
import lombok.Data;
@Data
public class UserPkRecordDetailDTO {
private Integer id;
}

View File

@@ -0,0 +1,10 @@
package vvpkassistant.User.model.DTO;
import lombok.Data;
@Data
public class UserPointsDetailDTO {
private Integer userId;
private Integer page;
private Integer size;
}

View File

@@ -0,0 +1,10 @@
package vvpkassistant.User.model.DTO;
import lombok.Data;
@Data
public class UserQueryMyAllPkDataDTO {
private Integer userId;
private Integer page;
private Integer size;
}

View File

@@ -0,0 +1,15 @@
package vvpkassistant.User.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@Data
@TableName("sign_in_records")
public class SignInRecord {
@TableId(type = IdType.AUTO)
private Integer id;
private Integer userId;
private Integer time;
}

View File

@@ -5,23 +5,111 @@ import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data; import lombok.Data;
import java.util.Date;
@Data @Data
@TableName("user") @TableName("system_users")
public class UserModel { public class UserModel {
@TableId(type = IdType.AUTO) @TableId(type = IdType.AUTO)
private Integer id; // 主键 private Integer id; // 主键
private String nickName; // 昵称 private String nickname; // 昵称
private String phoneNumber; // 手机号 private String mobile; // 手机号
private String headerIcon; // 头像
private String openid; // openid
private String sessionKey; // session key
private Integer status; // 用户状态 0 正常 其他业务逻辑待定 private Integer status; // 用户状态 0 正常 其他业务逻辑待定
private Long createTime; // 创建时间
private String userChatId; // 聊天使用的id使用微信的openid作为标识 private String userChatId; // 聊天使用的id使用微信的openid作为标识
private Integer points; // 用户积分 private Integer points; // 用户积分
private Integer inviterId; // 邀请人id
private String email; /**
* 用户账号
*/
private String username;
/**
* 密码
*/
private String password; private String password;
private Integer mailVerification;
private String userName; /**
* 备注
*/
private String remark;
/**
* 部门ID
*/
private Long deptId;
/**
* 岗位编号数组
*/
private String postIds;
/**
* 用户邮箱
*/
private String email;
/**
* 用户性别
*/
private Byte sex;
/**
* 头像地址
*/
private String avatar;
/**
* 最后登录IP
*/
private String loginIp;
/**
* 最后登录时间
*/
private Date loginDate;
/**
* 创建者
*/
private String creator;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新者
*/
private String updater;
/**
* 更新时间
*/
private Date updateTime;
/**
* 是否删除
*/
private Boolean deleted;
/**
* 租户编号
*/
private Long tenantId;
/**
* 能否登录主播爬虫客户端
*/
private Byte crawl;
/**
* 能否登录大哥爬虫客户端
*/
private Byte bigBrother;
/**
* 能否登录网页 AI
*/
private Byte webAi;
} }

View File

@@ -14,34 +14,5 @@ import vvpkassistant.mail.model.MailModel;
* @date: 2025/8/4 16:19 * @date: 2025/8/4 16:19
*/ */
public interface UserService extends IService<UserModel> { public interface UserService extends IService<UserModel> {
UserModelVO loginWithMail(UserModelDTO model);
UserModelVO updateUserInfo(UserModelDTO userModelDTO);
UserModelVO addUserWithMail(UserModelDTO model);
Boolean activateAccount(String token);
Boolean verificationMail(String token);
Object generatedQrcode();
Object checkQrcode(String uuid);
LoginInfoDTO scanQrcode(ScanInfoDTO scanInfoDTO);
void confirm(ScanInfoDTO scanInfoDTO);
void logOut(Integer id);
boolean setPassWord(UserModelDTO userModelDTO);
Object sendForgetPassWordMail(MailModel mailModel);
Object resetPassWord(UserModelDTO userModelDTO);
Boolean updateUserMail(MailModel mailModel);
Boolean checkUserName(UserModelDTO userModelDTO);
} }

View File

@@ -56,324 +56,4 @@ public class UserServiceImpl extends ServiceImpl<UserDao, UserModel> implements
.expireAfterWrite(2, TimeUnit.MINUTES) .expireAfterWrite(2, TimeUnit.MINUTES)
.build(); .build();
@Override
public UserModelVO loginWithMail(UserModelDTO model) {
if (model.getUserNameOrEmail().isEmpty()){
throw new BusinessException(ErrorCode.PARAMS_ERROR,"用户名或邮箱不能为空");
}
LambdaQueryWrapper<UserModel> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(UserModel::getEmail,model.getUserNameOrEmail())
.or()
.eq(UserModel::getUserName,model.getUserNameOrEmail())
.in(UserModel::getStatus, 0,2);
UserModel userModel = userDao.selectOne(lambdaQueryWrapper);
if (userModel == null) {
throw new BusinessException(ErrorCode.USER_DOES_NOT_EXIST);
}
String password = userModel.getPassword();
UserModelVO userModelVO = BeanUtil.copyProperties(userModel, UserModelVO.class);
if (BcryptUtils.matchPassword(password, model.getPassword())) {
StpUtil.login(userModel.getId());
userModelVO.setToken(StpUtil.getTokenValue());
userModelVO.setChatInfo(wxChatParam);
return userModelVO;
}else {
throw new BusinessException(ErrorCode.PASSWORD_ERROR);
}
}
@Override
public UserModelVO updateUserInfo(UserModelDTO userModelDTO) {
UserModel userInfo = userDao.selectById(userModelDTO.getId());
if (userInfo == null) {
throw new BusinessException(ErrorCode.USER_DOES_NOT_EXIST);
}
// 用户没有密码的情况下设置密码
if (userInfo.getPassword() == null && userModelDTO.getNewPassword() != null) {
if (!userModelDTO.getNewPassword().isEmpty()){
if (userModelDTO.getNewPassword().length()<6){
throw new BusinessException(ErrorCode.PARAMS_ERROR,"密码长度不能小于 6 位");
}
userModelDTO.setPassword(BcryptUtils.encryptPassword(userModelDTO.getNewPassword()));
}
}
if (userModelDTO.getEmail() != null) {
mailService.sendVerificationMail(userModelDTO.getEmail(), userModelDTO.getId());
}
// 用户有密码的情况下重新设置密码
if (userInfo.getPassword() != null && userModelDTO.getOldPassword() != null) {
if (BcryptUtils.matchPassword(userInfo.getPassword(),userModelDTO.getOldPassword())) {
userModelDTO.setPassword(BcryptUtils.encryptPassword(userModelDTO.getNewPassword()));
}else {
throw new BusinessException(ErrorCode.PASSWORD_ERROR,"旧密码不正确");
}
}
UserModel userModel = BeanUtil.copyProperties(userModelDTO, UserModel.class);
int i = userDao.updateById(userModel);
// 返回结果
UserModel afterUserInfo = userDao.selectById(userModel.getId());
UserModelVO userModelVO = BeanUtil.copyProperties(afterUserInfo, UserModelVO.class);
userModelVO.setNewAccount(false);
if (i == 1){
return userModelVO;
}else {
throw new BusinessException(ErrorCode.SYSTEM_ERROR);
}
}
@Override
public UserModelVO addUserWithMail(UserModelDTO userModelDTO) {
LambdaQueryWrapper<UserModel> lambdaQueryWrapper = new LambdaQueryWrapper<>();
LambdaQueryWrapper<UserModel> usernameWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(UserModel::getEmail,userModelDTO.getEmail());
UserModel userModel = userDao.selectOne(lambdaQueryWrapper);
UserModel usernameModel = userDao.selectOne(usernameWrapper
.eq(UserModel::getUserName, userModelDTO.getUserName()));
if (userModel != null) {
throw new BusinessException(ErrorCode.MAIL_ALREADY_EXIST);
}
if (usernameModel != null) {
throw new BusinessException(ErrorCode.USERNAME_ALREADY_EXIST);
}
if (userModelDTO.getPassword().length() < 6 ){
throw new BusinessException(ErrorCode.PARAMS_ERROR,"密码长度不能小于 6 位");
}
if (userModelDTO.getUserName().isEmpty()){
throw new BusinessException(ErrorCode.PARAMS_ERROR,"用户名不能为空");
}
userModelDTO.setPassword(BcryptUtils.encryptPassword(userModelDTO.getPassword()));
userModelDTO.setCreateTime(VVTools.currentTimeStamp());
//设置状态为待验证
userModelDTO.setStatus(2);
//设置积分为0
userModelDTO.setPoints(0);
UserModel userModelEntity = BeanUtil.copyProperties(userModelDTO, UserModel.class);
userModelEntity.setMailVerification(1);
if ( userDao.insert(userModelEntity) != 1){
throw new BusinessException(ErrorCode.ADD_FAILED,"用户注册失败");
}
mailService.sendMail(userModelDTO.getEmail(),userModelEntity.getId());
// 判断用户是否为邀请用户
if (userModelDTO.getInviterId() != null) {
UserModel oldUser = userDao.selectById(userModelDTO.getInviterId());
oldUser.setPoints(oldUser.getPoints() + 10);
userDao.updateById(oldUser);
}
UserModelVO userModelVO = BeanUtil.copyProperties(userModelEntity, UserModelVO.class);
StpUtil.login(userModelVO.getId());
userModelVO.setToken(StpUtil.getTokenValue());
userModelVO.setHavaPassword(true);
userModelVO.setNewAccount(true);
userModelVO.setChatInfo(wxChatParam);
log.info("用户{}注册,邮箱{},手机号{}",userModelVO.getId(),userModelVO.getEmail(),userModelVO.getPhoneNumber());
return userModelVO;
}
@Override
public Boolean activateAccount(String token) {
Integer userId = SaTempUtil.parseToken(token, Integer.class);
UserModel userModel = userDao.selectById(userId);
if (userModel == null) {
throw new BusinessException(ErrorCode.USER_DOES_NOT_EXIST);
}
if (userModel.getStatus() == 0){
throw new BusinessException(ErrorCode.USER_HAS_ACTIVATED);
}
userModel.setStatus(0);
userModel.setMailVerification(0);
if (userDao.updateById(userModel) == 1){
return true;
}else {
throw new BusinessException(ErrorCode.UPDATE_FAILED,"激活失败");
}
}
@Override
public Boolean verificationMail(String token) {
Integer userId = SaTempUtil.parseToken(token, Integer.class);
UserModel userModel = userDao.selectById(userId);
userModel.setMailVerification(0);
if (userDao.updateById(userModel) == 1){
return true;
}
throw new BusinessException(ErrorCode.SYSTEM_ERROR,"邮箱验证失败");
}
@Override
public QrcodeVO generatedQrcode() {
String uuid = UUID.randomUUID().toString();
QrcodeEntity qrcodeEntity = new QrcodeEntity();
qrcodeEntity.setUuid(uuid);
qrcodeEntity.setType("qrcdoe");
String base64QR = null;
try {
base64QR = QRCodeUtil.generateQRCode(JSONUtil.toJsonStr(qrcodeEntity), 200, 200);
} catch (WriterException | IOException e) {
log.error(e.getMessage());
throw new BusinessException(ErrorCode.SYSTEM_ERROR,"二维码生成失败");
}
LoginInfoDTO loginInfoDTO = new LoginInfoDTO();
loginInfoDTO.setStatus(LoginStatusEnum.UNSCANNED.name());
loginInfoDTO.setUuid(uuid);
// 二维码uuid绑定存入缓存
qrcodeCache.put(uuid,loginInfoDTO);
// 返回生成的二维码信息
QrcodeVO vo = QrcodeVO.builder().uuid(uuid).qrcode("data:image/png;base64," + base64QR).build();
log.info("-------生成二维码成功:{}-------", uuid);
return vo;
}
@Override
public Object checkQrcode(String uuid) {
LoginInfoDTO loginInfoDTO = qrcodeCache.getIfPresent(uuid);
if (loginInfoDTO == null) {
throw new BusinessException(ErrorCode.QRCODE_EXPIRED);
}
if (Objects.equals(loginInfoDTO.getStatus(), LoginStatusEnum.SCANNED.name())) {
return loginInfoDTO;
}
if (LoginStatusEnum.CONFIRMED.name().equals(loginInfoDTO.getStatus())) {
UserModel userModel = userDao.selectById(loginInfoDTO.getId());
StpUtil.login(userModel.getId());
UserModelVO userModelVO = BeanUtil.copyProperties(userModel, UserModelVO.class);
userModelVO.setToken(StpUtil.getTokenValue());
userModelVO.setChatInfo(wxChatParam);
return userModelVO;
}
return null;
}
@Override
public LoginInfoDTO scanQrcode(ScanInfoDTO scanInfoDTO) {
LoginInfoDTO loginInfoDTO = qrcodeCache.getIfPresent(scanInfoDTO.getUuid());
if (loginInfoDTO != null) {
loginInfoDTO.setStatus(LoginStatusEnum.SCANNED.name());
qrcodeCache.put(scanInfoDTO.getUuid(),loginInfoDTO);
}
log.info("-------扫码成功uuid:{}-------", scanInfoDTO.getUuid());
return loginInfoDTO;
}
@Override
public void confirm(ScanInfoDTO scanInfoDTO) {
LoginInfoDTO loginInfoDTO = qrcodeCache.getIfPresent(scanInfoDTO.getUuid());
if (loginInfoDTO != null) {
loginInfoDTO.setStatus(LoginStatusEnum.CONFIRMED.name());
loginInfoDTO.setId(scanInfoDTO.getId());
qrcodeCache.put(scanInfoDTO.getUuid(),loginInfoDTO);
}
log.info("-------确认登录成功uuid:{}-------", scanInfoDTO.getUuid());
}
@Override
public void logOut(Integer id) {
StpUtil.logout(id);
}
@Override
public boolean setPassWord(UserModelDTO userModelDTO) {
UserModel userModel = userDao.selectById(userModelDTO.getId());
if (userModel == null) {
throw new BusinessException(ErrorCode.USER_DOES_NOT_EXIST);
}
if (userModel.getPassword()!= null){
throw new BusinessException(ErrorCode.SYSTEM_ERROR,"已设置过密码");
}
if (userModelDTO.getPassword().length()< 6 ){
throw new BusinessException(ErrorCode.PARAMS_ERROR,"密码长度不能小于 6 位");
}
if (!Objects.equals(userModelDTO.getPassword(), userModelDTO.getConfirmPassword())) {
log.error("密码{},确认密码{}",userModelDTO.getPassword(),userModelDTO.getConfirmPassword());
throw new BusinessException(ErrorCode.PARAMS_ERROR,"两次密码输入不一致");
}else{
UserModel saveEntity = BeanUtil.copyProperties(userModelDTO, UserModel.class);
saveEntity.setPassword(BcryptUtils.encryptPassword(userModelDTO.getPassword()));
return userDao.updateById(saveEntity) == 1 ;
}
}
@Override
public Object sendForgetPassWordMail(MailModel mailModel) {
LambdaQueryWrapper<UserModel> lambdaQueryWrapper = new LambdaQueryWrapper<>();
UserModel userModel = userDao.selectOne(lambdaQueryWrapper
.eq(UserModel::getEmail, mailModel.getMailAddress())
.eq(UserModel::getStatus, 0)
.eq(UserModel::getMailVerification, 0));
if (userModel == null) {
throw new BusinessException(ErrorCode.USER_DOES_NOT_EXIST);
}
mailService.sendForgetPassWordMail(mailModel.getMailAddress(),userModel.getId());
return true;
}
@Override
public Object resetPassWord(UserModelDTO userModelDTO) {
Integer i = SaTempUtil.parseToken(userModelDTO.getToken(), Integer.class);
UserModel userModel = userDao.selectById(i);
if (userModel == null) {
throw new BusinessException(ErrorCode.USER_DOES_NOT_EXIST);
}
if (userModelDTO.getPassword().equals(userModelDTO.getConfirmPassword())) {
userModel.setPassword(BcryptUtils.encryptPassword(userModelDTO.getPassword()));
return userDao.updateById(userModel) == 1;
}
return false;
}
@Override
public Boolean updateUserMail(MailModel mailModel) {
String mail = CacheHolder.VERIFICATION_MAIL.getIfPresent(mailModel.getCode());
if (mail != null && mail.isEmpty()) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR,"验证码过期或验证码错误");
}
LambdaQueryWrapper<UserModel> duplicateMailUserWrapper = new LambdaQueryWrapper<>();
LambdaQueryWrapper<UserModel> lambdaQueryWrapper = new LambdaQueryWrapper<>();
UserModel duplicateMailUser = userDao.selectOne(duplicateMailUserWrapper
.eq(UserModel::getEmail, mailModel.getMailAddress()));
if (duplicateMailUser != null) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR,"邮箱地址已被使用");
}
UserModel userModel = userDao.selectOne(lambdaQueryWrapper
.eq(UserModel::getEmail, mail)
.eq(UserModel::getMailVerification, 0 ));
if (userModel == null) {
throw new BusinessException(ErrorCode.USER_MAIL_NOT_VERIFICATION);
}
userModel.setEmail(mailModel.getMailAddress());
mailService.sendVerificationMail(mailModel.getMailAddress(),userModel.getId());
userModel.setMailVerification(1);
return userDao.updateById(userModel) == 1;
}
@Override
public Boolean checkUserName(UserModelDTO userModelDTO) {
LambdaQueryWrapper<UserModel> lambdaQueryWrapper = new LambdaQueryWrapper<>();
UserModel userModel = userDao.selectOne(lambdaQueryWrapper
.eq(UserModel::getUserName, userModelDTO.getUserName()));
return userModel == null;
}
} }

View File

@@ -0,0 +1,25 @@
package vvpkassistant.chat.model.DTO;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
@Data
public class ChatReceiveImMessageDTO {
private String eventType;
private Map<String, Object> payload;
private final Map<String, Object> additionalFields = new HashMap<>();
@JsonAnySetter
public void putAdditionalField(String key, Object value) {
additionalFields.put(key, value);
}
@JsonAnyGetter
public Map<String, Object> getAdditionalFields() {
return additionalFields;
}
}

View File

@@ -1,5 +1,6 @@
package vvpkassistant.config; package vvpkassistant.config;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import vvpkassistant.FunctionConfig.mapper.FunctionConfigMapper; import vvpkassistant.FunctionConfig.mapper.FunctionConfigMapper;
@@ -11,9 +12,10 @@ import java.util.concurrent.CopyOnWriteArrayList;
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j
public class FunctionConfigHolder { public class FunctionConfigHolder {
// 线程安全的全局配置容器 // 线程安全的全局配置容器
public static final List<FunctionConfigModel> CONFIGS = new CopyOnWriteArrayList<>(); public static List<FunctionConfigModel> CONFIGS = new CopyOnWriteArrayList<>();
@Autowired @Autowired
private FunctionConfigMapper configMapper; private FunctionConfigMapper configMapper;
@@ -26,7 +28,7 @@ public class FunctionConfigHolder {
List<FunctionConfigModel> dbConfigs = configMapper.selectList(null); List<FunctionConfigModel> dbConfigs = configMapper.selectList(null);
CONFIGS.clear(); CONFIGS.clear();
CONFIGS.addAll(dbConfigs); CONFIGS.addAll(dbConfigs);
System.out.println("已加载 "+CONFIGS.size()+" 条功能配置"); log.info("已加载 {} 条功能配置", CONFIGS.size());
} }
/** /**

View File

@@ -0,0 +1,6 @@
package vvpkassistant.config;
public interface FunctionConfigProvider {
String getValue(String functionName);
}

View File

@@ -0,0 +1,12 @@
package vvpkassistant.config;
import org.springframework.stereotype.Component;
@Component
public class HolderBackedFunctionConfigProvider implements FunctionConfigProvider {
@Override
public String getValue(String functionName) {
return FunctionConfigHolder.getValue(functionName);
}
}

View File

@@ -59,7 +59,11 @@ public class SaTokenConfigure implements WebMvcConfigurer {
"/user/inputUserInfo", "/user/inputUserInfo",
"/user/resetPassword", "/user/resetPassword",
"/pk/pkList", "/pk/pkList",
"/systemMessage/list" "/systemMessage/list",
"/pk/pkListForPython",
"/pk/insertPkDetail",
"/pk/updatePkRecordInfo",
"/pk/grantPkResultPoints"
}; };
} }

View File

@@ -4,13 +4,14 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import vvpkassistant.Anchors.model.DTO.AnchorDeleteRequestDTO;
import vvpkassistant.Anchors.model.DTO.AnchorListRequestDTO;
import vvpkassistant.Anchors.model.AnchorModel; import vvpkassistant.Anchors.model.AnchorModel;
import vvpkassistant.Anchors.service.AnchorsService; import vvpkassistant.Anchors.service.AnchorsService;
import vvpkassistant.Data.ResponseData; import vvpkassistant.Data.ResponseData;
import vvpkassistant.common.ErrorCode; import vvpkassistant.common.ErrorCode;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.Map;
@RestController @RestController
@RequestMapping("anchor") @RequestMapping("anchor")
@@ -28,15 +29,15 @@ public class AnchorsController {
// 查询我的主播列表 // 查询我的主播列表
@PostMapping("list") @PostMapping("list")
public ResponseData<Object> myAnchorList(@RequestBody Map<String,Integer> map) { public ResponseData<Object> myAnchorList(@RequestBody AnchorListRequestDTO request) {
Integer userId = map.get("id"); Integer userId = request.getId();
return ResponseData.success(anchorsService.selectMyAnchor(userId)); return ResponseData.success(anchorsService.selectMyAnchor(userId));
} }
// 删除我的主播 // 删除我的主播
@PostMapping("deleteMyAnchor") @PostMapping("deleteMyAnchor")
public ResponseData<Object> deleteMyAnchor(@RequestBody Map<String,Integer> map) { public ResponseData<Object> deleteMyAnchor(@RequestBody AnchorDeleteRequestDTO request) {
Integer id = map.get("id"); Integer id = request.getId();
return anchorsService.deleteMyAnchor(id) == 1 ? ResponseData.success(""):ResponseData.error(ErrorCode.DELETE_FAILED ); return anchorsService.deleteMyAnchor(id) == 1 ? ResponseData.success(""):ResponseData.error(ErrorCode.DELETE_FAILED );
} }

View File

@@ -5,6 +5,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import vvpkassistant.Data.ResponseData; import vvpkassistant.Data.ResponseData;
import vvpkassistant.chat.mapper.ChatDao; import vvpkassistant.chat.mapper.ChatDao;
import vvpkassistant.chat.model.DTO.ChatReceiveImMessageDTO;
import vvpkassistant.chat.model.ChatModel; import vvpkassistant.chat.model.ChatModel;
import vvpkassistant.common.ErrorCode; import vvpkassistant.common.ErrorCode;
@@ -34,7 +35,7 @@ public class ChatController {
//接收im消息 //接收im消息
@PostMapping("receiveImMessage") @PostMapping("receiveImMessage")
public Map<String,Object> receiveImMessage(@RequestBody Map<String,Object> data) { public Map<String,Object> receiveImMessage(@RequestBody ChatReceiveImMessageDTO data) {
System.out.println(data); System.out.println(data);
Map<String,Object> result = new HashMap<>(); Map<String,Object> result = new HashMap<>();
result.put("code",200); result.put("code",200);

View File

@@ -1,18 +1,24 @@
package vvpkassistant.controller; package vvpkassistant.controller;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import vvpkassistant.CoinRecords.CoinRecordsDao;
import vvpkassistant.Data.ResponseData; import vvpkassistant.Data.ResponseData;
import vvpkassistant.Data.ResponseInfo; import vvpkassistant.Data.ResponseInfo;
import vvpkassistant.Tools.VVTools; import vvpkassistant.Tools.VVTools;
import vvpkassistant.User.mapper.UserDao;
import vvpkassistant.pk.mapper.PkInfoDao; import vvpkassistant.pk.mapper.PkInfoDao;
import vvpkassistant.pk.mapper.PkRecordDao; import vvpkassistant.pk.mapper.PkRecordDao;
import vvpkassistant.pk.mapper.PkRecordDetailDao; import vvpkassistant.pk.mapper.PkRecordDetailDao;
import vvpkassistant.pk.model.DTO.PkDeleteByIdDTO;
import vvpkassistant.pk.model.DTO.PkFetchDetailDTO;
import vvpkassistant.pk.model.DTO.PkInfoDetailDTO;
import vvpkassistant.pk.model.DTO.PkListRequestDTO;
import vvpkassistant.pk.model.DTO.PkListUninvitedDTO;
import vvpkassistant.pk.model.DTO.PkQueryMyCanUseDTO;
import vvpkassistant.pk.model.DTO.PkResultPointsDTO;
import vvpkassistant.pk.model.PkInfoModel; import vvpkassistant.pk.model.PkInfoModel;
import vvpkassistant.pk.model.PkRecord; import vvpkassistant.pk.model.PkRecord;
import vvpkassistant.pk.model.PkRecordDetail; import vvpkassistant.pk.model.PkRecordDetail;
import vvpkassistant.pk.service.PKService; import vvpkassistant.pk.service.PKService;
import vvpkassistant.pk.service.PkResultPointService;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.List; import java.util.List;
@@ -26,6 +32,9 @@ public class PkController {
@Resource @Resource
private PKService pkService; private PKService pkService;
@Resource
private PkResultPointService pkResultPointService;
@Autowired @Autowired
private PkInfoDao pkDao; private PkInfoDao pkDao;
@@ -35,12 +44,6 @@ public class PkController {
@Autowired @Autowired
private PkRecordDetailDao detailDao; private PkRecordDetailDao detailDao;
@Autowired
private UserDao userDao;
@Autowired
private CoinRecordsDao coinRecordsDao;
// 创建pk数据 // 创建pk数据
@PostMapping("addPkData") @PostMapping("addPkData")
public ResponseData<PkInfoModel> addPkData(@RequestBody PkInfoModel pkModel) { public ResponseData<PkInfoModel> addPkData(@RequestBody PkInfoModel pkModel) {
@@ -61,29 +64,29 @@ public class PkController {
// pk列表 // pk列表
@PostMapping("pkList") @PostMapping("pkList")
public ResponseData<Object> pkList(@RequestBody Map<String,Object> map) { public ResponseData<Object> pkList(@RequestBody PkListRequestDTO request) {
return ResponseData.success(pkService.getPKList(map)); return ResponseData.success(pkService.getPKList(request));
} }
// 查询用户发布的大于当前时间的pk数据 // 查询用户发布的大于当前时间的pk数据
@PostMapping("queryMyCanUsePkData") @PostMapping("queryMyCanUsePkData")
public ResponseData<Object> queryMyCanUsePkData(@RequestBody Map<String,Object> map) { public ResponseData<Object> queryMyCanUsePkData(@RequestBody PkQueryMyCanUseDTO request) {
Long time = VVTools.currentTimeStamp(); Long time = VVTools.currentTimeStamp();
Integer userId = (Integer) map.get("userId"); Integer userId = request.getUserId();
List<PkInfoModel> pkModels = pkDao.queryCanUseData(userId, time); List<PkInfoModel> pkModels = pkDao.queryCanUseData(userId, time);
return ResponseData.success(pkModels); return ResponseData.success(pkModels);
} }
//pk文章详情 //pk文章详情
@PostMapping("pkInfoDetail") @PostMapping("pkInfoDetail")
public ResponseData<Object> pkInfoDetail(@RequestBody Map<String, Integer> map) { public ResponseData<Object> pkInfoDetail(@RequestBody PkInfoDetailDTO request) {
return ResponseData.success(pkService.pkInfoDetail(map)); return ResponseData.success(pkService.pkInfoDetail(request));
} }
//删除自己的pk数据 (单个) //删除自己的pk数据 (单个)
@PostMapping("deletePkDataWithId") @PostMapping("deletePkDataWithId")
public ResponseData<Object> deletePkDataWithId(@RequestBody Map<String,Integer> map) { public ResponseData<Object> deletePkDataWithId(@RequestBody PkDeleteByIdDTO request) {
Integer id = map.get("id"); Integer id = request.getId();
PkInfoModel pkInfoModel = pkDao.selectById(id); PkInfoModel pkInfoModel = pkDao.selectById(id);
if (pkInfoModel.getPinExpireTime() > VVTools.currentTimeStamp()) { if (pkInfoModel.getPinExpireTime() > VVTools.currentTimeStamp()) {
@@ -110,16 +113,16 @@ public class PkController {
// 查询pk中每个场次的详细数据 // 查询pk中每个场次的详细数据
@PostMapping("fetchDetailPkDataWithId") @PostMapping("fetchDetailPkDataWithId")
public ResponseData<Object> fetchDetailPkDataWithId(@RequestBody Map<String,Integer> map) { public ResponseData<Object> fetchDetailPkDataWithId(@RequestBody PkFetchDetailDTO request) {
Integer id = map.get("id"); Integer id = request.getId();
List<PkRecordDetail> pkRecordDetails = recordDao.fetchDetailPkDataWithId(id); List<PkRecordDetail> pkRecordDetails = detailDao.queryDetail(id);
return ResponseData.success(pkRecordDetails); return ResponseData.success(pkRecordDetails);
} }
// 根据用户id查询该用户已发布的未被邀请的主播列表 // 根据用户id查询该用户已发布的未被邀请的主播列表
@PostMapping("listUninvitedPublishedAnchorsByUserId") @PostMapping("listUninvitedPublishedAnchorsByUserId")
public ResponseData<Object> listUninvitedPublishedAnchorsByUserId(@RequestBody Map<String,Integer> map) { public ResponseData<Object> listUninvitedPublishedAnchorsByUserId(@RequestBody PkListUninvitedDTO request) {
Integer userId = map.get("userId"); Integer userId = request.getUserId();
List<PkInfoModel> pkInfoModels = pkDao.listUninvitedPublishedAnchorsByUserId(userId); List<PkInfoModel> pkInfoModels = pkDao.listUninvitedPublishedAnchorsByUserId(userId);
for (PkInfoModel pkInfoModel : pkInfoModels) { for (PkInfoModel pkInfoModel : pkInfoModels) {
pkInfoModel.setDisPlayId(VVTools.replaceChar(pkInfoModel.getAnchorId(),'*')); pkInfoModel.setDisPlayId(VVTools.replaceChar(pkInfoModel.getAnchorId(),'*'));
@@ -149,6 +152,12 @@ public class PkController {
return i == 1 ? ResponseData.success("") : ResponseData.error(ResponseInfo.ERROR.getCode(),null); return i == 1 ? ResponseData.success("") : ResponseData.error(ResponseInfo.ERROR.getCode(),null);
} }
// 根据PK结果为用户增加积分
@PostMapping("grantPkResultPoints")
public ResponseData<Object> grantPkResultPoints(@RequestBody PkResultPointsDTO request) {
return ResponseData.success(pkResultPointService.grantPkResultPoints(request));
}
// 插入pk明细表数据 // 插入pk明细表数据
@PostMapping("insertPkDetail") @PostMapping("insertPkDetail")
public ResponseData<Object> insert(@RequestBody PkRecordDetail detail) { public ResponseData<Object> insert(@RequestBody PkRecordDetail detail) {

View File

@@ -0,0 +1,45 @@
package vvpkassistant.controller;
import cn.dev33.satoken.stp.StpUtil;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import vvpkassistant.Data.ResponseData;
import vvpkassistant.common.ErrorCode;
import vvpkassistant.exception.BusinessException;
import vvpkassistant.item.model.PkItem;
import vvpkassistant.item.model.DTO.PkItemPurchaseDTO;
import vvpkassistant.item.service.PkItemPurchaseService;
import vvpkassistant.item.service.PkItemService;
import javax.annotation.Resource;
import java.util.List;
@RestController
@RequestMapping("pkItem")
public class PkItemController {
@Resource
private PkItemService pkItemService;
@Resource
private PkItemPurchaseService pkItemPurchaseService;
@GetMapping("list")
public ResponseData<List<PkItem>> list() {
return ResponseData.success(pkItemService.selectItemList());
}
@PostMapping("buy")
public ResponseData<Object> buy(@RequestBody PkItemPurchaseDTO request) {
if (request == null || request.getItemId() == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "itemId不能为空");
}
long userId = StpUtil.getLoginIdAsLong();
return ResponseData.success(pkItemPurchaseService.purchase(userId, request.getItemId()));
}
}

View File

@@ -7,11 +7,11 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import vvpkassistant.Data.ResponseData; import vvpkassistant.Data.ResponseData;
import vvpkassistant.SystemMessage.mapper.SystemMessageDao; import vvpkassistant.SystemMessage.mapper.SystemMessageDao;
import vvpkassistant.SystemMessage.model.DTO.SystemMessageListRequestDTO;
import vvpkassistant.SystemMessage.model.SystemMessage; import vvpkassistant.SystemMessage.model.SystemMessage;
import vvpkassistant.Tools.VVTools; import vvpkassistant.Tools.VVTools;
import java.util.List; import java.util.List;
import java.util.Map;
@RestController @RestController
@RequestMapping("systemMessage") @RequestMapping("systemMessage")
@@ -22,9 +22,9 @@ public class SystemMessageController {
// 获取列表 // 获取列表
@PostMapping("list") @PostMapping("list")
public ResponseData<Object> messageList(@RequestBody Map<String,Integer> map) { public ResponseData<Object> messageList(@RequestBody SystemMessageListRequestDTO request) {
Integer page = map.get("page"); Integer page = request.getPage();
Integer size = map.get("size"); Integer size = request.getSize();
List<SystemMessage> systemMessages = messageDao.messageList(page * size, size); List<SystemMessage> systemMessages = messageDao.messageList(page * size, size);
return ResponseData.success(systemMessages); return ResponseData.success(systemMessages);
} }

View File

@@ -6,11 +6,19 @@ import org.springframework.web.bind.annotation.*;
import vvpkassistant.CoinRecords.CoinRecords; import vvpkassistant.CoinRecords.CoinRecords;
import vvpkassistant.CoinRecords.CoinRecordsDao; import vvpkassistant.CoinRecords.CoinRecordsDao;
import vvpkassistant.Data.ResponseData; import vvpkassistant.Data.ResponseData;
import vvpkassistant.Data.ResponseInfo;
import vvpkassistant.Data.WxChatParam; import vvpkassistant.Data.WxChatParam;
import vvpkassistant.User.mapper.UserDao; import vvpkassistant.User.mapper.UserDao;
import vvpkassistant.User.mapper.SignInRecordDao;
import vvpkassistant.User.model.DTO.ScanInfoDTO; import vvpkassistant.User.model.DTO.ScanInfoDTO;
import vvpkassistant.User.model.DTO.UserCancelPinDTO;
import vvpkassistant.User.model.DTO.UserHandlePkInfoDTO;
import vvpkassistant.User.model.DTO.UserInputUserInfoDTO;
import vvpkassistant.User.model.DTO.UserLoginWithPhoneNumberDTO;
import vvpkassistant.User.model.DTO.UserModelDTO; import vvpkassistant.User.model.DTO.UserModelDTO;
import vvpkassistant.User.model.DTO.UserPinToTopDTO;
import vvpkassistant.User.model.DTO.UserPkRecordDetailDTO;
import vvpkassistant.User.model.DTO.UserPointsDetailDTO;
import vvpkassistant.User.model.DTO.UserQueryMyAllPkDataDTO;
import vvpkassistant.User.model.UserModel; import vvpkassistant.User.model.UserModel;
import vvpkassistant.User.model.UserModelVO; import vvpkassistant.User.model.UserModelVO;
import vvpkassistant.User.service.UserService; import vvpkassistant.User.service.UserService;
@@ -22,9 +30,11 @@ import vvpkassistant.exception.BusinessException;
import vvpkassistant.mail.model.MailModel; import vvpkassistant.mail.model.MailModel;
import vvpkassistant.mail.service.MailService; import vvpkassistant.mail.service.MailService;
import vvpkassistant.pk.mapper.PkInfoDao; import vvpkassistant.pk.mapper.PkInfoDao;
import vvpkassistant.pk.mapper.PkRecordDao;
import vvpkassistant.pk.model.PkInfoModel; import vvpkassistant.pk.model.PkInfoModel;
import vvpkassistant.pk.model.PkRecordDetail; import vvpkassistant.pk.model.PkRecordDetail;
import vvpkassistant.pk.mapper.PkRecordDetailDao; import vvpkassistant.pk.mapper.PkRecordDetailDao;
import vvpkassistant.pk.service.PkPinService;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.HashMap; import java.util.HashMap;
@@ -44,9 +54,15 @@ public class UserController {
@Autowired @Autowired
private PkRecordDetailDao detailDao; private PkRecordDetailDao detailDao;
@Autowired
private PkRecordDao recordDao;
@Autowired @Autowired
private CoinRecordsDao coinRecordsDao; private CoinRecordsDao coinRecordsDao;
@Autowired
private SignInRecordDao signInRecordDao;
@Autowired @Autowired
private WxChatParam wxChatParam; private WxChatParam wxChatParam;
@@ -60,20 +76,23 @@ public class UserController {
@Resource @Resource
private MailService mailService; private MailService mailService;
@Resource
private PkPinService pkPinService;
// 配置用户信息 // 配置用户信息
@PostMapping("inputUserInfo") @PostMapping("inputUserInfo")
public ResponseData<Object> inputUserInfo(@RequestBody Map<String,Object> param) { public ResponseData<Object> inputUserInfo(@RequestBody UserInputUserInfoDTO param) {
if (!param.containsKey("code")) { if (param == null || param.getCode() == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR,"code不能为空"); throw new BusinessException(ErrorCode.PARAMS_ERROR,"code不能为空");
} }
if (!param.containsKey("id")) { if (param.getId() == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR,"id不能为空"); throw new BusinessException(ErrorCode.PARAMS_ERROR,"id不能为空");
} }
//获取前端传递过来的code //获取前端传递过来的code
String code = param.get("code").toString(); String code = param.getCode();
// 调用微信获取openid接口 // 调用微信获取openid接口
Map<String, Object> wx_result = vvRequester.loginApp(code); Map<String, Object> wx_result = vvRequester.loginApp(code);
@@ -105,15 +124,10 @@ public class UserController {
//查询用户 //查询用户
String openId = wx_result.get("openid").toString(); String openId = wx_result.get("openid").toString();
String sessionKey = wx_result.get("session_key").toString();
// 创建一个临时model // 创建一个临时model
UserModel tempModel = new UserModel(); UserModel tempModel = new UserModel();
tempModel.setId(Integer.valueOf(param.get("id").toString())); tempModel.setId(param.getId());
tempModel.setOpenid(openId);
tempModel.setHeaderIcon(param.get("headerIcon").toString());
tempModel.setNickName(param.get("nickName").toString());
tempModel.setSessionKey(sessionKey);
tempModel.setUserChatId(openId); tempModel.setUserChatId(openId);
int i = userDao.updateById(tempModel); int i = userDao.updateById(tempModel);
if (i == 1) { if (i == 1) {
@@ -137,13 +151,13 @@ public class UserController {
// 手机号登录 / 注册 // 手机号登录 / 注册
@PostMapping("loginWithPhoneNumber") @PostMapping("loginWithPhoneNumber")
public ResponseData<Object> loginWithPhoneNumber(@RequestBody Map<String,Object> param) { public ResponseData<Object> loginWithPhoneNumber(@RequestBody UserLoginWithPhoneNumberDTO param) {
if (!param.containsKey("code")) { if (param == null || param.getCode() == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR,"code不能为空"); throw new BusinessException(ErrorCode.PARAMS_ERROR,"code不能为空");
} }
String code = param.get("code").toString(); String code = param.getCode();
String phoneNumber = vvRequester.queryPhoneNumber(code); String phoneNumber = vvRequester.queryPhoneNumber(code);
if (phoneNumber.isEmpty()) { if (phoneNumber.isEmpty()) {
throw new BusinessException(ErrorCode.PARAMS_ERROR,"手机号码无法查询"); throw new BusinessException(ErrorCode.PARAMS_ERROR,"手机号码无法查询");
@@ -162,8 +176,6 @@ public class UserController {
return ResponseData.success(result); return ResponseData.success(result);
}else{ // 新用户 }else{ // 新用户
UserModel tempModel = new UserModel(); UserModel tempModel = new UserModel();
tempModel.setPhoneNumber(phoneNumber);
tempModel.setCreateTime(VVTools.currentTimeStamp());
//设置状态为正常 //设置状态为正常
tempModel.setStatus(0); tempModel.setStatus(0);
//设置积分为0 //设置积分为0
@@ -171,8 +183,8 @@ public class UserController {
userDao.insert(tempModel); userDao.insert(tempModel);
// 判断用户是否为邀请用户 // 判断用户是否为邀请用户
if (param.containsKey("inviterId")) { if (param.getInviterId() != null) {
int inviterId = (int) param.get("inviterId"); int inviterId = param.getInviterId();
// 查询用户增加积分 // 查询用户增加积分
UserModel oldUser = userDao.selectById(inviterId); UserModel oldUser = userDao.selectById(inviterId);
oldUser.setPoints(oldUser.getPoints() + 10); oldUser.setPoints(oldUser.getPoints() + 10);
@@ -196,8 +208,9 @@ public class UserController {
// 修改用户信息 // 修改用户信息
@PostMapping("updateUserInfo") @PostMapping("updateUserInfo")
public ResponseData<Object> updateUserInfo(@RequestBody UserModelDTO userModelDTO) { public ResponseData<Object> updateUserInfo(@RequestBody UserModelDTO userModelDTO) {
UserModelVO userModelVO = userService.updateUserInfo( userModelDTO); // UserModelVO userModelVO = userService.updateUserInfo( userModelDTO);
return ResponseData.success(userModelVO); // return ResponseData.success(userModelVO);
return null;
} }
// 获取用户信息 // 获取用户信息
@@ -211,16 +224,15 @@ public class UserController {
throw new BusinessException(ErrorCode.USER_DOES_NOT_EXIST); throw new BusinessException(ErrorCode.USER_DOES_NOT_EXIST);
} }
UserModelVO userModelVO = BeanUtil.copyProperties(userModel, UserModelVO.class); UserModelVO userModelVO = BeanUtil.copyProperties(userModel, UserModelVO.class);
userModelVO.setHavaPassword(userModel.getPassword() != null);
return ResponseData.success(userModelVO); return ResponseData.success(userModelVO);
} }
// 查询用户所有pk数据 // 查询用户所有pk数据
@PostMapping("queryMyAllPkData") @PostMapping("queryMyAllPkData")
public ResponseData<Object> queryMyAllPkData(@RequestBody Map<String,Integer> map) { public ResponseData<Object> queryMyAllPkData(@RequestBody UserQueryMyAllPkDataDTO request) {
Integer userId = map.get("userId"); Integer userId = request.getUserId();
Integer page = map.get("page"); Integer page = request.getPage();
Integer size = map.get("size"); Integer size = request.getSize();
List<PkInfoModel> pkInfoModels = pkInfoDao.queryAllPkData(userId, page * size, size); List<PkInfoModel> pkInfoModels = pkInfoDao.queryAllPkData(userId, page * size, size);
long currentTimeStamp = VVTools.currentTimeStamp(); long currentTimeStamp = VVTools.currentTimeStamp();
// 查找置顶的数据 // 查找置顶的数据
@@ -232,18 +244,18 @@ public class UserController {
// 查询我的pk记录 列表 // 查询我的pk记录 列表
@PostMapping("handlePkInfo") @PostMapping("handlePkInfo")
public ResponseData<Object> handlePkInfo(@RequestBody Map<String,Integer> map) { public ResponseData<Object> handlePkInfo(@RequestBody UserHandlePkInfoDTO request) {
Integer type = map.get("type"); Integer type = request.getType();
Integer id = map.get("userId"); Integer id = request.getUserId();
Integer page = map.get("page"); Integer page = request.getPage();
Integer size = map.get("size"); Integer size = request.getSize();
// 我发起的pk数据 // 我发起的pk数据
if (type == 1) { if (type == 1) {
return ResponseData.success(userDao.findCreatedPk(id, page * size, size)); return ResponseData.success(recordDao.findCreatedPk(id, page * size, size));
}else if (type == 2){ }else if (type == 2){
// 别人邀请我的pk数据 // 别人邀请我的pk数据
return ResponseData.success(userDao.getMyGuestPkList(id, page * size, size)); return ResponseData.success(recordDao.getMyGuestPkList(id, page * size, size));
} }
throw new BusinessException(ErrorCode.SYSTEM_ERROR); throw new BusinessException(ErrorCode.SYSTEM_ERROR);
} }
@@ -251,22 +263,22 @@ public class UserController {
// 查詢单条pk记录详情 // 查詢单条pk记录详情
@PostMapping("pkRecordDetail") @PostMapping("pkRecordDetail")
public ResponseData<Object> pkRecordDetail(@RequestBody Map<String,Integer> map) { public ResponseData<Object> pkRecordDetail(@RequestBody UserPkRecordDetailDTO request) {
Integer id = map.get("id"); Integer id = request.getId();
List<PkRecordDetail> pkRecordDetails = detailDao.queryDetail(id); List<PkRecordDetail> pkRecordDetails = detailDao.queryDetail(id);
return ResponseData.success(pkRecordDetails); return ResponseData.success(pkRecordDetails);
} }
// 签到 // 签到
@PostMapping("signIn") @PostMapping("signIn")
public ResponseData<Object> signIn(@RequestBody Map<String,Integer> map) { public ResponseData<Object> signIn() {
Integer userId = map.get("userId"); long userId = StpUtil.getLoginIdAsLong();
int i = userDao.checkSignStatus(userId); int i = signInRecordDao.checkSignStatus((int) userId);
if (i != 0) { if (i != 0) {
throw new BusinessException(ErrorCode.SIGN_IN_FAIL); throw new BusinessException(ErrorCode.SIGN_IN_FAIL);
} }
int result = userDao.signIn(userId); int result = signInRecordDao.signIn((int) userId);
UserModel userModel = userDao.selectById(userId); UserModel userModel = userDao.selectById(userId);
int count = Integer.parseInt(FunctionConfigHolder.getValue("签到增加积分")); int count = Integer.parseInt(FunctionConfigHolder.getValue("签到增加积分"));
@@ -278,7 +290,7 @@ public class UserController {
if (result == 1) { if (result == 1) {
// 增加记录 // 增加记录
CoinRecords coinRecords = new CoinRecords("签到增加积分",userId,count, (int) VVTools.currentTimeStamp(),1); CoinRecords coinRecords = new CoinRecords("签到增加积分", (int) userId,count, (int) VVTools.currentTimeStamp(),1);
coinRecordsDao.insert(coinRecords); coinRecordsDao.insert(coinRecords);
return ResponseData.success(null); return ResponseData.success(null);
}else { }else {
@@ -289,197 +301,139 @@ public class UserController {
// 查询用户当天签到状态 // 查询用户当天签到状态
@GetMapping("checkSignStatus") @GetMapping("checkSignStatus")
public ResponseData<Object> checkSignStatus(Integer userId) { public ResponseData<Object> checkSignStatus(Integer userId) {
int i = userDao.checkSignStatus(userId); int i = signInRecordDao.checkSignStatus(userId);
return i == 0 ? ResponseData.success(true) : ResponseData.success(false); return i == 0 ? ResponseData.success(true) : ResponseData.success(false);
} }
// 置顶文章 // 置顶文章
@PostMapping("pinToTop") @PostMapping("pinToTop")
public ResponseData<Object> pinToTop(@RequestBody Map<String,Integer> map) { public ResponseData<Object> pinToTop(@RequestBody UserPinToTopDTO request) {
// 文章id if (request == null || request.getArticleId() == null || request.getPinExpireTime() == null) {
Integer articleId = map.get("articleId"); throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数不能为空");
PkInfoModel pkInfoModel = pkInfoDao.selectById(articleId);
Integer userId = pkInfoModel.getSenderId();
// 到期时间戳
Integer pinExpireTime = map.get("pinExpireTime");
long currentTimeStamp = VVTools.currentTimeStamp();
long hour = VVTools.calculateHoursRound(pinExpireTime, currentTimeStamp);
String coin = FunctionConfigHolder.getValue("置顶扣除积分");
int totalCoin = (int) (Integer.parseInt(coin) * hour);
UserModel userModel = userDao.selectById(userId);
if (userModel != null) {
// 扣除积分 更新数据
Integer points = userModel.getPoints();
if (points - totalCoin > 0) {
userModel.setPoints(userModel.getPoints() - totalCoin);
userDao.updateById(userModel);
// 设置置顶到期时间
pkInfoModel.setPinExpireTime(pinExpireTime);
// 设置创建置顶的时间
pkInfoModel.setPinCreateTime((int) VVTools.currentTimeStamp());
// 更新pk文章数据
int i = pkInfoDao.updateById(pkInfoModel);
if (i == 1) {
String info = String.format("置顶成功,扣除%d积分",totalCoin);
// 增加积分变动记录
CoinRecords coinRecords = new CoinRecords("置顶扣除积分",userId,totalCoin, (int) VVTools.currentTimeStamp(),0);
coinRecordsDao.insert(coinRecords);
// 返回给前端数据
return ResponseData.success(info);
}else {
throw new BusinessException(ErrorCode.SYSTEM_ERROR);
}
}else {
throw new BusinessException(ErrorCode.SYSTEM_ERROR,String.format("积分不足,需要%d积分",totalCoin));
}
}else {
throw new BusinessException(ErrorCode.SYSTEM_ERROR,"用户不存在");
} }
int operatorUserId = Integer.parseInt(StpUtil.getLoginId().toString());
String info = pkPinService.pinToTop(operatorUserId, request.getArticleId(), request.getPinExpireTime());
return ResponseData.success(info);
} }
// 取消置顶 // 取消置顶
@PostMapping("cancelPin") @PostMapping("cancelPin")
public ResponseData<Object> cancelPin(@RequestBody Map<String,Integer> map) { public ResponseData<Object> cancelPin(@RequestBody UserCancelPinDTO request) {
Integer articleId = map.get("articleId"); if (request == null || request.getArticleId() == null) {
PkInfoModel pkInfoModel = pkInfoDao.selectById(articleId); throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数不能为空");
Integer pinExpireTime = pkInfoModel.getPinExpireTime();
long hour = VVTools.calculateHoursFloor(pinExpireTime, VVTools.currentTimeStamp());
String coin = FunctionConfigHolder.getValue("置顶扣除积分");
// 计算总积分。用于返还给用户
int totalCoin = (int) (Integer.parseInt(coin) * hour);
// 获取用户对象
UserModel userModel = userDao.selectById(pkInfoModel.getSenderId());
Integer points = userModel.getPoints();
// 返还用户积分
userModel.setPoints(points + totalCoin);
// 更新数据库
userDao.updateById(userModel);
// 重置置顶时间
pkInfoModel.setPinExpireTime(0);
pkInfoModel.setPinCreateTime(0);
int i = pkInfoDao.updateById(pkInfoModel);
if (i == 1) {
// 添加积分更变相关记录
CoinRecords coinRecords = new CoinRecords("取消置顶返还积分",pkInfoModel.getSenderId(),totalCoin, (int) VVTools.currentTimeStamp(),1);
coinRecordsDao.insert(coinRecords);
return ResponseData.success(String.format("操作成功,返还%d积分",totalCoin));
}else {
return ResponseData.error(ResponseInfo.ERROR.getCode(),null);
} }
int operatorUserId = Integer.parseInt(StpUtil.getLoginId().toString());
String info = pkPinService.cancelPin(operatorUserId, request.getArticleId());
return ResponseData.success(info);
} }
// 获取积分明细 // 获取积分明细
@PostMapping("pointsDetail") @PostMapping("pointsDetail")
public ResponseData<Object> pointsDetail(@RequestBody Map<String,Integer> map) { public ResponseData<Object> pointsDetail(@RequestBody UserPointsDetailDTO request) {
Integer userId = map.get("userId"); Integer userId = request.getUserId();
Integer page = map.get("page"); Integer page = request.getPage();
Integer size = map.get("size"); Integer size = request.getSize();
List<CoinRecords> coinRecords = coinRecordsDao.fetchMyPointsData(userId, page * size, size); List<CoinRecords> coinRecords = coinRecordsDao.fetchMyPointsData(userId, page * size, size);
return ResponseData.success(coinRecords); return ResponseData.success(coinRecords);
} }
// 邮件登录接口 //// 邮件登录接口
@PostMapping("/loginWithMail") // @PostMapping("/loginWithMail")
public ResponseData<Object> loginWithMail(@RequestBody UserModelDTO model) { // public ResponseData<Object> loginWithMail(@RequestBody UserModelDTO model) {
return ResponseData.success(userService.loginWithMail(model)); // return ResponseData.success(userService.loginWithMail(model));
} // }
//
//邮件注册接口 ////邮件注册接口
@PostMapping("/registerWithMail") // @PostMapping("/registerWithMail")
public ResponseData<Object> mailRegister(@RequestBody UserModelDTO model){ // public ResponseData<Object> mailRegister(@RequestBody UserModelDTO model){
return ResponseData.success(userService.addUserWithMail(model)); // return ResponseData.success(userService.addUserWithMail(model));
} // }
//
// 用户邮件激活 //// 用户邮件激活
@GetMapping("/activate") // @GetMapping("/activate")
public ResponseData<Object> activateAccount(@RequestParam("token") String token){ // public ResponseData<Object> activateAccount(@RequestParam("token") String token){
return ResponseData.success(userService.activateAccount(token)); // return ResponseData.success(userService.activateAccount(token));
} // }
//
// 重发邮件接口 //// 重发邮件接口
@PostMapping("/resendMail") // @PostMapping("/resendMail")
public ResponseData<Object> resendMail(@RequestBody MailModel mailModel){ // public ResponseData<Object> resendMail(@RequestBody MailModel mailModel){
return ResponseData.success(mailService.resendMail(mailModel)); // return ResponseData.success(mailService.resendMail(mailModel));
} // }
//
// 验证邮箱链接 //// 验证邮箱链接
@GetMapping("/verificationMail") // @GetMapping("/verificationMail")
public ResponseData<Object> verificationMail(@RequestParam("token") String token){ // public ResponseData<Object> verificationMail(@RequestParam("token") String token){
return ResponseData.success(userService.verificationMail(token)); // return ResponseData.success(userService.verificationMail(token));
} // }
//
// 发送忘记密码邮件 //// 发送忘记密码邮件
@PostMapping("/forgetMail") // @PostMapping("/forgetMail")
public ResponseData<Object> sendForgetPassWordMail(@RequestBody MailModel mailModel){ // public ResponseData<Object> sendForgetPassWordMail(@RequestBody MailModel mailModel){
return ResponseData.success(userService.sendForgetPassWordMail(mailModel)); // return ResponseData.success(userService.sendForgetPassWordMail(mailModel));
} // }
//
// 生成二维码登录接口 //// 生成二维码登录接口
@GetMapping("/qrcode") // @GetMapping("/qrcode")
public ResponseData<Object> generatedQrcode(){ // public ResponseData<Object> generatedQrcode(){
return ResponseData.success(userService.generatedQrcode()); // return ResponseData.success(userService.generatedQrcode());
} // }
//
// 检查二维码状态接口 //// 检查二维码状态接口
@GetMapping("/check/{uuid}") // @GetMapping("/check/{uuid}")
public ResponseData<Object> checkQrcode(@PathVariable String uuid){ // public ResponseData<Object> checkQrcode(@PathVariable String uuid){
return ResponseData.success(userService.checkQrcode(uuid)); // return ResponseData.success(userService.checkQrcode(uuid));
} // }
//
// 二维码扫描状态 //// 二维码扫描状态
@PostMapping("/scan") // @PostMapping("/scan")
public ResponseData<?> scanQrCode(@RequestBody ScanInfoDTO scanInfoDTO) { // public ResponseData<?> scanQrCode(@RequestBody ScanInfoDTO scanInfoDTO) {
return ResponseData.success( userService.scanQrcode(scanInfoDTO)); // return ResponseData.success( userService.scanQrcode(scanInfoDTO));
} // }
//
//二维码扫描确认 ////二维码扫描确认
@PostMapping("/confirm") // @PostMapping("/confirm")
public ResponseData<?> confirm(@RequestBody ScanInfoDTO scanInfoDTO) { // public ResponseData<?> confirm(@RequestBody ScanInfoDTO scanInfoDTO) {
userService.confirm(scanInfoDTO); // userService.confirm(scanInfoDTO);
return ResponseData.success(""); // return ResponseData.success("");
} // }
//
// 注销接口 //// 注销接口
@PostMapping("/logout") // @PostMapping("/logout")
public ResponseData<Object> logOut(@RequestBody UserModelDTO userModelDTO){ // public ResponseData<Object> logOut(@RequestBody UserModelDTO userModelDTO){
userService.logOut(userModelDTO.getId()); // userService.logOut(userModelDTO.getId());
return ResponseData.success(""); // return ResponseData.success("");
} // }
//
// 设置密码接口 //// 设置密码接口
@PostMapping("/setPassword") // @PostMapping("/setPassword")
public ResponseData<Object>setPassWord(@RequestBody UserModelDTO userModelDTO){ // public ResponseData<Object>setPassWord(@RequestBody UserModelDTO userModelDTO){
return ResponseData.success(userService.setPassWord(userModelDTO)); // return ResponseData.success(userService.setPassWord(userModelDTO));
} // }
//
// 用户通过邮件验证链接重设密码接口 //// 用户通过邮件验证链接重设密码接口
@PostMapping("/resetPassword") // @PostMapping("/resetPassword")
public ResponseData<Object>resetPassWord(@RequestBody UserModelDTO userModelDTO){ // public ResponseData<Object>resetPassWord(@RequestBody UserModelDTO userModelDTO){
return ResponseData.success(userService.resetPassWord(userModelDTO)); // return ResponseData.success(userService.resetPassWord(userModelDTO));
} // }
//
// 设置新邮箱接口 //// 设置新邮箱接口
@PostMapping("/updateUserMail") // @PostMapping("/updateUserMail")
public ResponseData<Object>updateUserMail(@RequestBody MailModel mailModel){ // public ResponseData<Object>updateUserMail(@RequestBody MailModel mailModel){
return ResponseData.success(userService.updateUserMail(mailModel)); // return ResponseData.success(userService.updateUserMail(mailModel));
} // }
//
// 发送更新邮箱验证邮件接口 //// 发送更新邮箱验证邮件接口
@PostMapping("/sendUpdateMailConfirmMail") // @PostMapping("/sendUpdateMailConfirmMail")
public ResponseData<Object>sendUpdateMailConfirmMail(@RequestBody MailModel mailModel){ // public ResponseData<Object>sendUpdateMailConfirmMail(@RequestBody MailModel mailModel){
return ResponseData.success(mailService.sendUpdateConfirmMail(mailModel)); // return ResponseData.success(mailService.sendUpdateConfirmMail(mailModel));
} // }
//
// 检查重复用户名接口 //// 检查重复用户名接口
@PostMapping("/checkUserName") // @PostMapping("/checkUserName")
public ResponseData<Boolean>checkUserName(@RequestBody UserModelDTO userModelDTO){ // public ResponseData<Boolean>checkUserName(@RequestBody UserModelDTO userModelDTO){
return ResponseData.success(userService.checkUserName(userModelDTO)); // return ResponseData.success(userService.checkUserName(userModelDTO));
} // }
} }

View File

@@ -0,0 +1,27 @@
package vvpkassistant.item;
import org.apache.ibatis.annotations.Mapper;
import vvpkassistant.item.model.PkItem;
import java.util.List;
/*
* @author: ziin
* @date: 2026/3/27 10:13
*/
@Mapper
public interface PkItemMapper {
int deleteByPrimaryKey(Long id);
int insert(PkItem record);
int insertSelective(PkItem record);
PkItem selectByPrimaryKey(Long id);
List<PkItem> selectItemList();
int updateByPrimaryKeySelective(PkItem record);
int updateByPrimaryKey(PkItem record);
}

View File

@@ -0,0 +1,9 @@
package vvpkassistant.item.model.DTO;
import lombok.Data;
@Data
public class PkItemPurchaseDTO {
private Long itemId;
}

View File

@@ -0,0 +1,78 @@
package vvpkassistant.item.model;
import lombok.Data;
import java.util.Date;
/*
* @author: ziin
* @date: 2026/3/27 10:13
*/
@Data
public class PkItem {
/**
* 主键Id
*/
private Long id;
/**
* 套餐名称
*/
private String itemName;
/**
* 套餐价格
*/
private Integer itemPrice;
/**
* 套餐描述
*/
private String itemDesc;
/**
* 功能
*/
private String itemFunction;
/**
* 时长
*/
private Integer itemDuration;
/**
* 是否上架
*/
private Integer itemStatus;
/**
* 备注
*/
private String remark;
/**
* 创建者
*/
private String creator;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新者
*/
private String updater;
/**
* 更新时间
*/
private Date updateTime;
/**
* 是否删除
*/
private Boolean deleted;
}

View File

@@ -0,0 +1,6 @@
package vvpkassistant.item.service;
public interface PkItemPurchaseService {
String purchase(Long userId, Long itemId);
}

View File

@@ -0,0 +1,284 @@
package vvpkassistant.item.service;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import vvpkassistant.CoinRecords.CoinRecords;
import vvpkassistant.CoinRecords.CoinRecordsDao;
import vvpkassistant.Tools.EpochSecondProvider;
import vvpkassistant.User.mapper.UserDao;
import vvpkassistant.User.model.UserModel;
import vvpkassistant.common.ErrorCode;
import vvpkassistant.exception.BusinessException;
import vvpkassistant.item.PkItemMapper;
import vvpkassistant.item.model.PkItem;
import vvpkassistant.iterm_recoder.mapper.PkItemRecoderMapper;
import vvpkassistant.iterm_recoder.model.PkItemRecoder;
import vvpkassistant.tenant.mapper.SystemTenantMapper;
import vvpkassistant.tenant.model.SystemTenant;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Service
public class PkItemPurchaseServiceImpl implements PkItemPurchaseService {
private static final byte ENABLED = 1;
private static final int COIN_RECORD_DEDUCT = 0;
private static final long SECONDS_PER_HOUR = 3600L;
private static final ZoneId SHANGHAI_ZONE_ID = ZoneId.of("Asia/Shanghai");
private final PkItemMapper pkItemMapper;
private final UserDao userDao;
private final SystemTenantMapper systemTenantMapper;
private final CoinRecordsDao coinRecordsDao;
private final PkItemRecoderMapper pkItemRecoderMapper;
private final EpochSecondProvider epochSecondProvider;
public PkItemPurchaseServiceImpl(
PkItemMapper pkItemMapper,
UserDao userDao,
SystemTenantMapper systemTenantMapper,
CoinRecordsDao coinRecordsDao,
PkItemRecoderMapper pkItemRecoderMapper,
EpochSecondProvider epochSecondProvider
) {
this.pkItemMapper = pkItemMapper;
this.userDao = userDao;
this.systemTenantMapper = systemTenantMapper;
this.coinRecordsDao = coinRecordsDao;
this.pkItemRecoderMapper = pkItemRecoderMapper;
this.epochSecondProvider = epochSecondProvider;
}
@Override
@Transactional(rollbackFor = Exception.class)
public String purchase(Long userId, Long itemId) {
validateRequest(userId, itemId);
PkItem item = loadAvailableItem(itemId);
UserModel user = loadUser(userId);
ItemFunction function = resolveFunction(item.getItemFunction());
long now = epochSecondProvider.nowEpochSecond();
ensureTenantBound(user);
ensureNotPurchasedToday(userId, itemId, now);
deductPoints(userId, item.getItemPrice());
enableUserFunction(user.getId(), function);
extendTenantExpireTime(user.getTenantId(), function, item.getItemDuration(), now);
insertConsumeRecord(userId, item, now);
insertPurchaseRecord(userId, itemId, now);
return "购买成功:" + item.getItemName();
}
private static void validateRequest(Long userId, Long itemId) {
if (userId == null || itemId == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数不能为空");
}
}
private PkItem loadAvailableItem(Long itemId) {
PkItem item = pkItemMapper.selectByPrimaryKey(itemId);
if (item == null || Boolean.TRUE.equals(item.getDeleted()) || item.getItemStatus() == null || item.getItemStatus() != 0) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "商品不存在或已下架");
}
if (item.getItemPrice() == null || item.getItemPrice() < 0) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "商品价格配置错误");
}
if (item.getItemDuration() == null || item.getItemDuration() <= 0) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "商品时长配置错误");
}
return item;
}
private UserModel loadUser(Long userId) {
UserModel user = userDao.selectById(userId);
if (user == null) {
throw new BusinessException(ErrorCode.USER_DOES_NOT_EXIST);
}
return user;
}
private static void ensureTenantBound(UserModel user) {
if (user.getTenantId() == null) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "用户未绑定租户");
}
}
private void ensureNotPurchasedToday(Long userId, Long itemId, long now) {
Map<String, Long> dayRange = todayRangeInShanghai(now);
Date startTime = toDate(dayRange.get("start"));
Date endTime = toDate(dayRange.get("end"));
long count = pkItemRecoderMapper.countTodayPurchase(userId, String.valueOf(itemId), startTime, endTime);
if (count > 0) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "该商品今日已购买");
}
}
private void deductPoints(Long userId, Integer itemPrice) {
int intUserId = toIntId(userId);
if (userDao.decreasePointsIfEnough(intUserId, itemPrice) == 1) {
return;
}
if (userDao.selectById(userId) == null) {
throw new BusinessException(ErrorCode.USER_DOES_NOT_EXIST);
}
throw new BusinessException(ErrorCode.SYSTEM_ERROR, String.format("积分不足,需要%d积分", itemPrice));
}
private void enableUserFunction(Integer userId, ItemFunction function) {
UserModel updateModel = new UserModel();
updateModel.setId(userId);
switch (function) {
case CRAWL:
updateModel.setCrawl(ENABLED);
break;
case WEB_AI:
updateModel.setWebAi(ENABLED);
break;
case BIG_BROTHER:
updateModel.setBigBrother(ENABLED);
break;
default:
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "暂不支持的商品功能");
}
if (userDao.updateById(updateModel) != 1) {
throw new BusinessException(ErrorCode.UPDATE_FAILED, "更新用户功能失败");
}
}
private void extendTenantExpireTime(Long tenantId, ItemFunction function, Integer durationHours, long now) {
SystemTenant tenant = systemTenantMapper.selectById(tenantId);
if (tenant == null || Boolean.TRUE.equals(tenant.getDeleted())) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "租户不存在");
}
Date expireTime = calculateExpireTime(getExpireTime(tenant, function), durationHours, now);
SystemTenant updateModel = new SystemTenant();
updateModel.setId(tenantId);
applyExpireTime(updateModel, function, expireTime);
if (systemTenantMapper.updateById(updateModel) != 1) {
throw new BusinessException(ErrorCode.UPDATE_FAILED, "更新租户功能时长失败");
}
}
private void insertConsumeRecord(Long userId, PkItem item, long now) {
CoinRecords coinRecords = new CoinRecords(
"购买商品:" + item.getItemName(),
toIntId(userId),
item.getItemPrice(),
toIntEpoch(now),
COIN_RECORD_DEDUCT
);
if (coinRecordsDao.insert(coinRecords) != 1) {
throw new BusinessException(ErrorCode.ADD_FAILED, "写入消费记录失败");
}
}
private void insertPurchaseRecord(Long userId, Long itemId, long now) {
Date nowDate = toDate(now);
PkItemRecoder recoder = new PkItemRecoder();
recoder.setUserId(userId);
recoder.setItemId(String.valueOf(itemId));
recoder.setCreator(String.valueOf(userId));
recoder.setCreateTime(nowDate);
recoder.setUpdater(String.valueOf(userId));
recoder.setUpdateTime(nowDate);
recoder.setDeleted(Boolean.FALSE);
if (pkItemRecoderMapper.insert(recoder) != 1) {
throw new BusinessException(ErrorCode.ADD_FAILED, "写入购买记录失败");
}
}
private static Date calculateExpireTime(Date currentExpireTime, Integer durationHours, long now) {
long durationSeconds = Math.multiplyExact(durationHours.longValue(), SECONDS_PER_HOUR);
long baseEpochSecond = resolveBaseEpochSecond(currentExpireTime, now);
return toDate(Math.addExact(baseEpochSecond, durationSeconds));
}
private static long resolveBaseEpochSecond(Date currentExpireTime, long now) {
if (currentExpireTime == null) {
return now;
}
return Math.max(currentExpireTime.toInstant().getEpochSecond(), now);
}
private static Date getExpireTime(SystemTenant tenant, ItemFunction function) {
switch (function) {
case CRAWL:
return tenant.getCrawlExpireTime();
case WEB_AI:
return tenant.getAiExpireTime();
case BIG_BROTHER:
return tenant.getBrotherExpireTime();
default:
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "暂不支持的商品功能");
}
}
private static void applyExpireTime(SystemTenant tenant, ItemFunction function, Date expireTime) {
switch (function) {
case CRAWL:
tenant.setCrawlExpireTime(expireTime);
return;
case WEB_AI:
tenant.setAiExpireTime(expireTime);
return;
case BIG_BROTHER:
tenant.setBrotherExpireTime(expireTime);
return;
default:
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "暂不支持的商品功能");
}
}
private static ItemFunction resolveFunction(String itemFunction) {
if ("crawl".equalsIgnoreCase(itemFunction)) {
return ItemFunction.CRAWL;
}
if ("webAi".equalsIgnoreCase(itemFunction) || "web_ai".equalsIgnoreCase(itemFunction) || "ai".equalsIgnoreCase(itemFunction)) {
return ItemFunction.WEB_AI;
}
if ("bigBrother".equalsIgnoreCase(itemFunction) || "big_brother".equalsIgnoreCase(itemFunction) || "brother".equalsIgnoreCase(itemFunction)) {
return ItemFunction.BIG_BROTHER;
}
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "未知的商品功能类型:" + itemFunction);
}
private static Date toDate(long epochSecond) {
return Date.from(Instant.ofEpochSecond(epochSecond));
}
private static Map<String, Long> todayRangeInShanghai(long epochSecond) {
LocalDate targetDate = Instant.ofEpochSecond(epochSecond)
.atZone(SHANGHAI_ZONE_ID)
.toLocalDate();
long start = targetDate.atStartOfDay(SHANGHAI_ZONE_ID).toEpochSecond();
long end = targetDate.plusDays(1).atStartOfDay(SHANGHAI_ZONE_ID).toEpochSecond() - 1;
Map<String, Long> dayRange = new HashMap<>();
dayRange.put("start", start);
dayRange.put("end", end);
return dayRange;
}
private static int toIntId(Long id) {
if (id > Integer.MAX_VALUE) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "用户编号超出范围");
}
return id.intValue();
}
private static int toIntEpoch(long epochSecond) {
if (epochSecond > Integer.MAX_VALUE) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "时间超出范围");
}
return (int) epochSecond;
}
private enum ItemFunction {
CRAWL,
WEB_AI,
BIG_BROTHER
}
}

View File

@@ -0,0 +1,13 @@
package vvpkassistant.item.service;
import vvpkassistant.item.model.PkItem;
import java.util.List;
/*
* @author: ziin
* @date: 2026/3/27 10:13
*/
public interface PkItemService {
List<PkItem> selectItemList();
}

View File

@@ -0,0 +1,24 @@
package vvpkassistant.item.service;
import org.springframework.stereotype.Service;
import vvpkassistant.item.PkItemMapper;
import vvpkassistant.item.model.PkItem;
import javax.annotation.Resource;
import java.util.List;
/*
* @author: ziin
* @date: 2026/3/27 10:13
*/
@Service
public class PkItemServiceImpl implements PkItemService {
@Resource
private PkItemMapper pkItemMapper;
@Override
public List<PkItem> selectItemList() {
return pkItemMapper.selectItemList();
}
}

View File

@@ -0,0 +1,21 @@
package vvpkassistant.iterm_recoder.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import vvpkassistant.iterm_recoder.model.PkItemRecoder;
import java.util.Date;
@Mapper
public interface PkItemRecoderMapper extends BaseMapper<PkItemRecoder> {
@Select("select count(1) from pk_item_recoder where user_Id = #{userId} and item_id = #{itemId} and deleted = 0 and create_time between #{startTime} and #{endTime}")
long countTodayPurchase(
@Param("userId") Long userId,
@Param("itemId") String itemId,
@Param("startTime") Date startTime,
@Param("endTime") Date endTime
);
}

View File

@@ -0,0 +1,33 @@
package vvpkassistant.iterm_recoder.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
@Data
@TableName("pk_item_recoder")
public class PkItemRecoder {
@TableId(value = "Id", type = IdType.ASSIGN_ID)
private Long id;
@TableField("user_id")
private Long userId;
@TableField("item_id")
private String itemId;
private String creator;
private Date createTime;
private String updater;
private Date updateTime;
private Boolean deleted;
}

View File

@@ -7,14 +7,14 @@ import vvpkassistant.mail.model.MailModel;
* @date: 2025/8/4 15:42 * @date: 2025/8/4 15:42
*/ */
public interface MailService { public interface MailService {
//
void sendMail(String emailAddress,Integer userId); // void sendMail(String emailAddress,Integer userId);
//
void sendVerificationMail(String emailAddress,Integer userId); // void sendVerificationMail(String emailAddress,Integer userId);
//
Boolean resendMail(MailModel mailModel); // Boolean resendMail(MailModel mailModel);
//
void sendForgetPassWordMail(String mailAddress, Integer id); // void sendForgetPassWordMail(String mailAddress, Integer id);
//
Object sendUpdateConfirmMail(MailModel mailModel); // Object sendUpdateConfirmMail(MailModel mailModel);
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,8 @@
package vvpkassistant.pk.mapper; package vvpkassistant.pk.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.*; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.apache.ibatis.annotations.Mapper;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import vvpkassistant.pk.model.PkInfoModel; import vvpkassistant.pk.model.PkInfoModel;
@@ -11,68 +13,135 @@ import java.util.Map;
public interface PkInfoDao extends BaseMapper<PkInfoModel> { public interface PkInfoDao extends BaseMapper<PkInfoModel> {
//根据条件筛选pk列表数据 //根据条件筛选pk列表数据
@Select("<script>" + default List<PkInfoModel> selectPkInfoByCondition(
"SELECT * FROM pk_info " + int page,
"WHERE 1=1 " + int size,
" <if test='condition.sex != null'> AND sex = #{condition.sex} </if>" + // 性别筛选 Map<String, Object> condition,
" <if test='condition.coin != null'> " + // 金币筛选 long todayStart,
" AND coin BETWEEN #{condition.coin.start} AND #{condition.coin.end} " + long todayEnd
" </if>" + ) {
" <if test='condition.country != null'> AND country = #{condition.country} </if>" + LambdaQueryWrapper<PkInfoModel> wrapper = Wrappers.<PkInfoModel>lambdaQuery();
" <if test='condition.pkTime != null'> " + // pk时间筛选 if (condition != null) {
" AND pk_time BETWEEN #{condition.pkTime.start} AND #{condition.pkTime.end} " + Object sex = condition.get("sex");
" </if>" + if (sex != null) {
" <if test='condition.type == 1'> " + // 当天时间 wrapper.eq(PkInfoModel::getSex, sex);
" AND pk_time BETWEEN #{todayStart} AND #{todayEnd}" + }
" </if>" +
" <if test='condition.type == 2'> " + // 大于当天 Map<String, Object> coin = asMap(condition.get("coin"));
" AND pk_time > #{todayEnd}" + Long coinStart = asLong(coin == null ? null : coin.get("start"));
" </if>" + Long coinEnd = asLong(coin == null ? null : coin.get("end"));
" AND invite_status = 0 " + if (coinStart != null && coinEnd != null) {
"ORDER BY pin_expire_time > UNIX_TIMESTAMP() DESC, " + wrapper.between(PkInfoModel::getCoin, coinStart, coinEnd);
"CASE WHEN pin_expire_time > UNIX_TIMESTAMP() THEN pin_create_time ELSE NULL END ASC, " + }
"id DESC " +
"LIMIT #{page} , #{size}" + Object country = condition.get("country");
"</script>") if (country != null) {
List<PkInfoModel> selectPkInfoByCondition( wrapper.eq(PkInfoModel::getCountry, country);
@Param("page") int page, }
@Param("size") int size,
@Param("condition") Map<String, Object> condition, Map<String, Object> pkTime = asMap(condition.get("pkTime"));
@Param("todayStart") long todayStart, // 当天开始时间戳00:00:00 Long pkTimeStart = asLong(pkTime == null ? null : pkTime.get("start"));
@Param("todayEnd") long todayEnd // 当天结束时间戳23:59:59 Long pkTimeEnd = asLong(pkTime == null ? null : pkTime.get("end"));
); if (pkTimeStart != null && pkTimeEnd != null) {
wrapper.between(PkInfoModel::getPkTime, pkTimeStart, pkTimeEnd);
}
Integer type = asInteger(condition.get("type"));
if (type != null) {
if (type == 1) {
wrapper.between(PkInfoModel::getPkTime, todayStart, todayEnd);
} else if (type == 2) {
wrapper.gt(PkInfoModel::getPkTime, todayEnd);
}
}
}
wrapper.eq(PkInfoModel::getInviteStatus, 0);
wrapper.last(String.format(
"ORDER BY pin_expire_time > UNIX_TIMESTAMP() DESC, " +
"CASE WHEN pin_expire_time > UNIX_TIMESTAMP() THEN pin_create_time ELSE NULL END ASC, " +
"id DESC LIMIT %d, %d",
page, size));
return selectList(wrapper);
}
// 查询用户发布的大于当前时间的pk数据 // 查询用户发布的大于当前时间的pk数据
@Select("select * from pk_info where #{userId} = sender_id and #{time} <= pk_time and invite_status = 0;") default List<PkInfoModel> queryCanUseData(Integer userId, Long time) {
List<PkInfoModel> queryCanUseData(@Param("userId") Integer userId , @Param("time") Long time); return selectList(Wrappers.<PkInfoModel>lambdaQuery()
.eq(PkInfoModel::getSenderId, userId)
.ge(PkInfoModel::getPkTime, time)
.eq(PkInfoModel::getInviteStatus, 0));
}
// 查询用户发布的所有pk数据 // 查询用户发布的所有pk数据
@Select("select * from pk_info where sender_id = #{userId} order by id desc limit #{page}, #{size};") default List<PkInfoModel> queryAllPkData(Integer userId, Integer page, Integer size) {
List<PkInfoModel> queryAllPkData(@Param("userId") Integer userId, @Param("page") Integer page, @Param("size") Integer size); return selectList(Wrappers.<PkInfoModel>lambdaQuery()
.eq(PkInfoModel::getSenderId, userId)
.orderByDesc(PkInfoModel::getId)
.last(String.format("limit %d, %d", page, size)));
}
// 根据id删除pk信息 // 根据id删除pk信息
@Delete("delete from pk_info where id = #{id}") default Integer deletePkDataWithId(Integer id) {
Integer deletePkDataWithId(@Param("id") Integer id); return deleteById(id);
}
//查询制定时间范围制定主播的pk信息 //查询制定时间范围制定主播的pk信息
@Select("select * from pk_info where anchor_id = #{anchorId} and pk_time between #{startTime} and #{endTime}") default List<PkInfoModel> selectDataWithAnchorIdAndTime(String anchorId, long startTime, long endTime) {
List<PkInfoModel> selectDataWithAnchorIdAndTime(@Param("anchorId") String anchorId, @Param("startTime") long startTime , @Param("endTime") long endTime); return selectList(Wrappers.<PkInfoModel>lambdaQuery()
.eq(PkInfoModel::getAnchorId, anchorId)
.between(PkInfoModel::getPkTime, startTime, endTime));
}
// 置顶和取消置顶 // 置顶和取消置顶
@Update("update pk_info set pin_expire_time = #{time} where id = #{id}") default int setPinTime(Integer id, @Nullable Integer time) {
int setPinTime(@Param("id") Integer id, @Param("time") @Nullable Integer time); PkInfoModel updateModel = new PkInfoModel();
updateModel.setPinExpireTime(time);
return update(updateModel, Wrappers.<PkInfoModel>lambdaUpdate()
.eq(PkInfoModel::getId, id));
}
// 根据用户id查询该用户已发布的未被邀请的主播列表 // 根据用户id查询该用户已发布的未被邀请的主播列表
@Select("select * from pk_info where sender_id = #{userId} and invite_status = 0 and pk_time > UNIX_TIMESTAMP();") default List<PkInfoModel> listUninvitedPublishedAnchorsByUserId(Integer userId) {
List<PkInfoModel> listUninvitedPublishedAnchorsByUserId(@Param("userId") Integer userId); long currentTime = System.currentTimeMillis() / 1000;
return selectList(Wrappers.<PkInfoModel>lambdaQuery()
.eq(PkInfoModel::getSenderId, userId)
.eq(PkInfoModel::getInviteStatus, 0)
.gt(PkInfoModel::getPkTime, currentTime));
}
// 查询当前用户与该主播是否存在未完成的pk记录 static Map<String, Object> asMap(Object value) {
@Select("SELECT COUNT(*) FROM `pk_record`\n" + if (value instanceof Map) {
"WHERE (user_id_a = #{userId} OR user_id_b = #{userId})\n" + return (Map<String, Object>) value;
"AND (anchor_id_a = #{anchorId} OR anchor_id_b = #{anchorId})\n" + }
"AND pk_status = 1\n" + return null;
"AND pk_time > UNIX_TIMESTAMP()") }
Integer checkIfUnfinishedPKExistsWithAnchor(@Param("userId") Integer userId, @Param("anchorId") String anchorId);
static Long asLong(Object value) {
if (value == null) {
return null;
}
if (value instanceof Number) {
return ((Number) value).longValue();
}
try {
return Long.parseLong(value.toString());
} catch (NumberFormatException e) {
return null;
}
}
static Integer asInteger(Object value) {
if (value == null) {
return null;
}
if (value instanceof Number) {
return ((Number) value).intValue();
}
try {
return Integer.parseInt(value.toString());
} catch (NumberFormatException e) {
return null;
}
}
} }

View File

@@ -1,10 +1,8 @@
package vvpkassistant.pk.mapper; package vvpkassistant.pk.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import vvpkassistant.pk.model.PkRecordDetail;
import vvpkassistant.pk.model.PkRecord; import vvpkassistant.pk.model.PkRecord;
import java.util.List; import java.util.List;
@@ -14,28 +12,70 @@ import java.util.List;
public interface PkRecordDao extends BaseMapper<PkRecord> { public interface PkRecordDao extends BaseMapper<PkRecord> {
// 查询当天所有的pk数据 Python使用 // 查询当天所有的pk数据 Python使用
@Select("SELECT * FROM pk_record WHERE pk_time BETWEEN #{startTime} AND #{endTime} and pk_status = 1;") default List<PkRecord> pkListForToday(long start, long end) {
List<PkRecord> pkListForToday(@Param("startTime") long start, @Param("endTime") long end); return selectList(Wrappers.<PkRecord>lambdaQuery()
.between(PkRecord::getPkTime, String.valueOf(start), String.valueOf(end))
.eq(PkRecord::getPkStatus, 1));
}
// 查询大于等于今天的PK记录数据 // 查询大于等于今天的PK记录数据
@Select("select * from pk_record where (user_id_a = #{userId} or user_id_b = #{userId}) and pk_time >= #{fromTime}") default List<PkRecord> fetchDataFromTodayWithUserId(Integer userId, Long fromTime) {
List<PkRecord> fetchDataFromTodayWithUserId(@Param("userId") Integer userId, @Param("fromTime") Long fromTime); return selectList(Wrappers.<PkRecord>lambdaQuery()
.and(wrapper -> wrapper.eq(PkRecord::getUserIdA, userId)
.or()
.eq(PkRecord::getUserIdB, userId))
.ge(PkRecord::getPkTime, String.valueOf(fromTime)));
}
// 查询是否存在未处理的邀请数据 // 查询是否存在未处理的邀请数据
@Select("select count(*) from pk_record where anchor_id_a = #{anchorA} and anchor_id_b = #{anchorB} and pk_status = 0") default Integer getPendingInvitations(String anchorA, String anchorB) {
Integer getPendingInvitations(@Param("anchorA") String anchorA , @Param("anchorB") String anchorB); return Math.toIntExact(selectCount(Wrappers.<PkRecord>lambdaQuery()
.eq(PkRecord::getAnchorIdA, anchorA)
.eq(PkRecord::getAnchorIdB, anchorB)
.eq(PkRecord::getPkStatus, 0)));
}
// 根据id查询记录详情 // 根据id查询记录详情
@Select("select * from pk_record where id = #{id}") default PkRecord singleRecord(Integer id) {
PkRecord singleRecord(@Param("id") Integer id); return selectById(id);
}
// 我邀请的pk数据
default List<PkRecord> getMyGuestPkList(Integer userId, Integer page, Integer size) {
return selectList(Wrappers.<PkRecord>lambdaQuery()
.eq(PkRecord::getUserIdB, userId)
.orderByDesc(PkRecord::getId)
.last(String.format("limit %d, %d", page, size)));
}
// 查询每场pk的详细数据 // 我发起的pk数据
@Select("select * from pk_record_detail where pk_record_id = #{id}") default List<PkRecord> findCreatedPk(Integer userId, Integer page, Integer size) {
List<PkRecordDetail> fetchDetailPkDataWithId(@Param("id") Integer id); return selectList(Wrappers.<PkRecord>lambdaQuery()
.eq(PkRecord::getUserIdA, userId)
.orderByDesc(PkRecord::getId)
.last(String.format("limit %d, %d", page, size)));
}
// 查询当前用户与该主播是否存在未完成的pk记录
default int checkIfUnfinishedPKExistsWithAnchor(Integer userId, String anchorId) {
long currentTime = System.currentTimeMillis() / 1000;
return Math.toIntExact(selectCount(Wrappers.<PkRecord>lambdaQuery()
.and(wrapper -> wrapper.eq(PkRecord::getUserIdA, userId)
.or()
.eq(PkRecord::getUserIdB, userId))
.and(wrapper -> wrapper.eq(PkRecord::getAnchorIdA, anchorId)
.or()
.eq(PkRecord::getAnchorIdB, anchorId))
.eq(PkRecord::getPkStatus, 1)
.gt(PkRecord::getPkTime, String.valueOf(currentTime))));
}
// 查询主播是否存在pk记录 // 查询主播是否存在pk记录
@Select("select count(*) from pk_record where anchor_id_a = #{id} or anchor_id_b = #{id}") default int existsPkRecordByAnchor(String id) {
int existsPkRecordByAnchor(@Param("id") String id); return Math.toIntExact(selectCount(Wrappers.<PkRecord>lambdaQuery()
.and(wrapper -> wrapper.eq(PkRecord::getAnchorIdA, id)
.or()
.eq(PkRecord::getAnchorIdB, id))));
}
} }

View File

@@ -1,9 +1,8 @@
package vvpkassistant.pk.mapper; package vvpkassistant.pk.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import vvpkassistant.pk.model.PkRecordDetail; import vvpkassistant.pk.model.PkRecordDetail;
import java.util.List; import java.util.List;
@@ -12,7 +11,9 @@ import java.util.List;
public interface PkRecordDetailDao extends BaseMapper<PkRecordDetail> { public interface PkRecordDetailDao extends BaseMapper<PkRecordDetail> {
// 根据id查询对应的明细数据 // 根据id查询对应的明细数据
@Select("select * from pk_record_detail where pk_record_id = #{id}") default List<PkRecordDetail> queryDetail(Integer id) {
List<PkRecordDetail> queryDetail(@Param("id") Integer id); return selectList(Wrappers.<PkRecordDetail>lambdaQuery()
.eq(PkRecordDetail::getPkRecordId, id));
}
} }

View File

@@ -0,0 +1,8 @@
package vvpkassistant.pk.model.DTO;
import lombok.Data;
@Data
public class PkDeleteByIdDTO {
private Integer id;
}

View File

@@ -0,0 +1,8 @@
package vvpkassistant.pk.model.DTO;
import lombok.Data;
@Data
public class PkFetchDetailDTO {
private Integer id;
}

View File

@@ -0,0 +1,10 @@
package vvpkassistant.pk.model.DTO;
import lombok.Data;
@Data
public class PkInfoDetailDTO {
private Integer id;
private Integer userId;
private Integer from;
}

View File

@@ -0,0 +1,13 @@
package vvpkassistant.pk.model.DTO;
import lombok.Data;
import java.util.Map;
@Data
public class PkListRequestDTO {
private Integer page;
private Integer size;
private Map<String, Object> condition;
private Integer userId;
}

View File

@@ -0,0 +1,8 @@
package vvpkassistant.pk.model.DTO;
import lombok.Data;
@Data
public class PkListUninvitedDTO {
private Integer userId;
}

View File

@@ -0,0 +1,8 @@
package vvpkassistant.pk.model.DTO;
import lombok.Data;
@Data
public class PkQueryMyCanUseDTO {
private Integer userId;
}

View File

@@ -0,0 +1,10 @@
package vvpkassistant.pk.model.DTO;
import lombok.Data;
@Data
public class PkResultPointsDTO {
private Integer winnerUserId;
private Integer loserUserId;
private Boolean draw;
}

View File

@@ -1,11 +1,12 @@
package vvpkassistant.pk.service; package vvpkassistant.pk.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import vvpkassistant.pk.model.DTO.PkInfoDetailDTO;
import vvpkassistant.pk.model.DTO.PkListRequestDTO;
import vvpkassistant.pk.model.PkInfoModel; import vvpkassistant.pk.model.PkInfoModel;
import vvpkassistant.pk.model.PkRecord; import vvpkassistant.pk.model.PkRecord;
import java.util.List; import java.util.List;
import java.util.Map;
/* /*
* @author: ziin * @author: ziin
@@ -18,7 +19,7 @@ public interface PKService extends IService<PkInfoModel> {
PkRecord createPKRecord(PkRecord record); PkRecord createPKRecord(PkRecord record);
List<PkInfoModel> getPKList(Map<String, Object> map); List<PkInfoModel> getPKList(PkListRequestDTO request);
PkInfoModel pkInfoDetail(Map<String, Integer> map); PkInfoModel pkInfoDetail(PkInfoDetailDTO request);
} }

View File

@@ -4,8 +4,6 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import vvpkassistant.CoinRecords.CoinRecords; import vvpkassistant.CoinRecords.CoinRecords;
import vvpkassistant.CoinRecords.CoinRecordsDao; import vvpkassistant.CoinRecords.CoinRecordsDao;
import vvpkassistant.Data.ResponseData;
import vvpkassistant.Data.ResponseInfo;
import vvpkassistant.Tools.VVTools; import vvpkassistant.Tools.VVTools;
import vvpkassistant.User.mapper.UserDao; import vvpkassistant.User.mapper.UserDao;
import vvpkassistant.User.model.UserModel; import vvpkassistant.User.model.UserModel;
@@ -14,6 +12,8 @@ import vvpkassistant.config.FunctionConfigHolder;
import vvpkassistant.exception.BusinessException; import vvpkassistant.exception.BusinessException;
import vvpkassistant.pk.mapper.PkInfoDao; import vvpkassistant.pk.mapper.PkInfoDao;
import vvpkassistant.pk.mapper.PkRecordDao; import vvpkassistant.pk.mapper.PkRecordDao;
import vvpkassistant.pk.model.DTO.PkInfoDetailDTO;
import vvpkassistant.pk.model.DTO.PkListRequestDTO;
import vvpkassistant.pk.model.PkInfoModel; import vvpkassistant.pk.model.PkInfoModel;
import vvpkassistant.pk.model.PkRecord; import vvpkassistant.pk.model.PkRecord;
@@ -147,11 +147,11 @@ public class PKServiceImpl extends ServiceImpl<PkInfoDao, PkInfoModel> implement
} }
@Override @Override
public List<PkInfoModel> getPKList(Map<String, Object> map) { public List<PkInfoModel> getPKList(PkListRequestDTO request) {
Integer page = (Integer) map.get("page"); Integer page = request.getPage();
Integer size = (Integer) map.get("size"); Integer size = request.getSize();
Map<String,Object> condition = (Map<String, Object>) map.get("condition"); Map<String,Object> condition = request.getCondition();
Map<String, Long> todayTimeStampMap = VVTools.startAndEndTimeStampForToday(); Map<String, Long> todayTimeStampMap = VVTools.startAndEndTimeStampForToday();
Long start = VVTools.currentTimeStamp(); Long start = VVTools.currentTimeStamp();
@@ -162,9 +162,9 @@ public class PKServiceImpl extends ServiceImpl<PkInfoDao, PkInfoModel> implement
long currentTimeStamp = VVTools.currentTimeStamp(); long currentTimeStamp = VVTools.currentTimeStamp();
//如果传了用户id //如果传了用户id
if (map.containsKey("userId")) { if (request.getUserId() != null) {
Long begin = VVTools.currentTimeStamp(); Long begin = VVTools.currentTimeStamp();
Integer userId = Integer.valueOf(map.get("userId").toString()); Integer userId = request.getUserId();
// 查询出当前用户大于等于今天的已接受邀请的pk数据 // 查询出当前用户大于等于今天的已接受邀请的pk数据
List<PkRecord> pkRecords = pkRecordDao.fetchDataFromTodayWithUserId(userId, begin); List<PkRecord> pkRecords = pkRecordDao.fetchDataFromTodayWithUserId(userId, begin);
// 遍历查询出的数据。如果文章的id相同。就显示完整的主播名称 // 遍历查询出的数据。如果文章的id相同。就显示完整的主播名称
@@ -202,10 +202,10 @@ public class PKServiceImpl extends ServiceImpl<PkInfoDao, PkInfoModel> implement
} }
@Override @Override
public PkInfoModel pkInfoDetail(Map<String, Integer> map) { public PkInfoModel pkInfoDetail(PkInfoDetailDTO request) {
Integer id = map.get("id"); Integer id = request.getId();
Integer userId = map.get("userId"); Integer userId = request.getUserId();
Integer from = map.get("from"); // 1 首页 2 聊天 Integer from = request.getFrom(); // 1 首页 2 聊天
PkInfoModel pkInfoModel = pkInfoDao.selectById(id); PkInfoModel pkInfoModel = pkInfoDao.selectById(id);
if (pkInfoModel == null) { if (pkInfoModel == null) {
@@ -219,7 +219,7 @@ public class PKServiceImpl extends ServiceImpl<PkInfoDao, PkInfoModel> implement
pkInfoModel.setDisPlayId(pkInfoModel.getAnchorId()); pkInfoModel.setDisPlayId(pkInfoModel.getAnchorId());
} else { } else {
// 查询是否存在未完成的pk记录 // 查询是否存在未完成的pk记录
Integer isHave = pkInfoDao.checkIfUnfinishedPKExistsWithAnchor(userId, pkInfoModel.getAnchorId()); Integer isHave = pkRecordDao.checkIfUnfinishedPKExistsWithAnchor(userId, pkInfoModel.getAnchorId());
if (isHave > 0) { if (isHave > 0) {
pkInfoModel.setDisPlayId(pkInfoModel.getAnchorId()); pkInfoModel.setDisPlayId(pkInfoModel.getAnchorId());
} else { } else {
@@ -231,7 +231,7 @@ public class PKServiceImpl extends ServiceImpl<PkInfoDao, PkInfoModel> implement
throw new BusinessException(ErrorCode.SYSTEM_ERROR,"当前信息已无效"); throw new BusinessException(ErrorCode.SYSTEM_ERROR,"当前信息已无效");
} }
} else { } else {
Integer isHave = pkInfoDao.checkIfUnfinishedPKExistsWithAnchor(userId, pkInfoModel.getAnchorId()); Integer isHave = pkRecordDao.checkIfUnfinishedPKExistsWithAnchor(userId, pkInfoModel.getAnchorId());
if (isHave > 0) { if (isHave > 0) {
pkInfoModel.setDisPlayId(pkInfoModel.getAnchorId()); pkInfoModel.setDisPlayId(pkInfoModel.getAnchorId());
} else { } else {
@@ -240,4 +240,5 @@ public class PKServiceImpl extends ServiceImpl<PkInfoDao, PkInfoModel> implement
return pkInfoModel; return pkInfoModel;
} }
} }
} }

View File

@@ -0,0 +1,8 @@
package vvpkassistant.pk.service;
public interface PkPinService {
String pinToTop(int operatorUserId, int articleId, int pinExpireTime);
String cancelPin(int operatorUserId, int articleId);
}

View File

@@ -0,0 +1,167 @@
package vvpkassistant.pk.service;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import vvpkassistant.CoinRecords.CoinRecords;
import vvpkassistant.CoinRecords.CoinRecordsDao;
import vvpkassistant.Tools.EpochSecondProvider;
import vvpkassistant.Tools.VVTools;
import vvpkassistant.User.mapper.UserDao;
import vvpkassistant.User.model.UserModel;
import vvpkassistant.common.ErrorCode;
import vvpkassistant.config.FunctionConfigProvider;
import vvpkassistant.exception.BusinessException;
import vvpkassistant.pk.mapper.PkInfoDao;
import vvpkassistant.pk.model.PkInfoModel;
@Service
public class PkPinServiceImpl implements PkPinService {
private static final String PIN_COIN_CONFIG_NAME = "置顶扣除积分";
private static final int COIN_RECORD_ADD = 1;
private static final int COIN_RECORD_DEDUCT = 0;
private final PkInfoDao pkInfoDao;
private final UserDao userDao;
private final CoinRecordsDao coinRecordsDao;
private final FunctionConfigProvider functionConfigProvider;
private final EpochSecondProvider epochSecondProvider;
public PkPinServiceImpl(
PkInfoDao pkInfoDao,
UserDao userDao,
CoinRecordsDao coinRecordsDao,
FunctionConfigProvider functionConfigProvider,
EpochSecondProvider epochSecondProvider
) {
this.pkInfoDao = pkInfoDao;
this.userDao = userDao;
this.coinRecordsDao = coinRecordsDao;
this.functionConfigProvider = functionConfigProvider;
this.epochSecondProvider = epochSecondProvider;
}
@Override
@Transactional(rollbackFor = Exception.class)
public String pinToTop(int operatorUserId, int articleId, int pinExpireTime) {
long now = epochSecondProvider.nowEpochSecond();
ensureExpireTimeValid(pinExpireTime, now);
PkInfoModel pkInfoModel = pkInfoDao.selectById(articleId);
if (pkInfoModel == null) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "该信息不存在");
}
ensureOwner(operatorUserId, pkInfoModel.getSenderId());
int costPerHour = getPinCoinPerHour();
long hours = VVTools.calculateHoursRound(pinExpireTime, now);
int totalCoin = toIntCoin(multiplyCoin(costPerHour, hours, "置顶时长过长"), "置顶时长过长");
ensureDeductPoints(operatorUserId, totalCoin);
updatePinTime(pkInfoModel, pinExpireTime, (int) now);
insertCoinRecord("置顶扣除积分", operatorUserId, totalCoin, (int) now, COIN_RECORD_DEDUCT);
return String.format("置顶成功,扣除%d积分", totalCoin);
}
@Override
@Transactional(rollbackFor = Exception.class)
public String cancelPin(int operatorUserId, int articleId) {
long now = epochSecondProvider.nowEpochSecond();
PkInfoModel pkInfoModel = pkInfoDao.selectById(articleId);
if (pkInfoModel == null) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "该信息不存在");
}
ensureOwner(operatorUserId, pkInfoModel.getSenderId());
int costPerHour = getPinCoinPerHour();
int pinExpireTime = pkInfoModel.getPinExpireTime() == null ? 0 : pkInfoModel.getPinExpireTime();
long hours = VVTools.calculateHoursFloor(pinExpireTime, now);
int refundCoin = toIntCoin(multiplyCoin(costPerHour, hours, "返还积分计算溢出"), "返还积分计算溢出");
if (refundCoin > 0) {
int updated = userDao.increasePoints(operatorUserId, refundCoin);
if (updated != 1) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "用户不存在");
}
}
pkInfoModel.setPinExpireTime(0);
pkInfoModel.setPinCreateTime(0);
if (pkInfoDao.updateById(pkInfoModel) != 1) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR);
}
insertCoinRecord("取消置顶返还积分", operatorUserId, refundCoin, (int) now, COIN_RECORD_ADD);
return String.format("操作成功,返还%d积分", refundCoin);
}
private static void ensureExpireTimeValid(int pinExpireTime, long now) {
if (pinExpireTime <= now) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "置顶到期时间必须大于当前时间");
}
}
private static void ensureOwner(int operatorUserId, Integer senderId) {
if (senderId == null || senderId != operatorUserId) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "无权限操作");
}
}
private int getPinCoinPerHour() {
String value = functionConfigProvider.getValue(PIN_COIN_CONFIG_NAME);
if (value == null) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "未配置置顶扣除积分");
}
int coin;
try {
coin = Integer.parseInt(value);
} catch (NumberFormatException e) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "置顶扣除积分配置错误");
}
if (coin <= 0) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "置顶扣除积分配置错误");
}
return coin;
}
private static long multiplyCoin(int costPerHour, long hours, String overflowMessage) {
try {
return Math.multiplyExact((long) costPerHour, hours);
} catch (ArithmeticException e) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, overflowMessage);
}
}
private static int toIntCoin(long coin, String overflowMessage) {
if (coin > Integer.MAX_VALUE) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, overflowMessage);
}
return (int) coin;
}
private void ensureDeductPoints(int operatorUserId, int totalCoin) {
int updated = userDao.decreasePointsIfEnough(operatorUserId, totalCoin);
if (updated == 1) {
return;
}
UserModel userModel = userDao.selectById(operatorUserId);
if (userModel == null) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "用户不存在");
}
throw new BusinessException(ErrorCode.SYSTEM_ERROR, String.format("积分不足,需要%d积分", totalCoin));
}
private void updatePinTime(PkInfoModel pkInfoModel, int pinExpireTime, int now) {
pkInfoModel.setPinExpireTime(pinExpireTime);
pkInfoModel.setPinCreateTime(now);
if (pkInfoDao.updateById(pkInfoModel) != 1) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR);
}
}
private void insertCoinRecord(String desc, int userId, int coin, int now, int type) {
coinRecordsDao.insert(new CoinRecords(desc, userId, coin, now, type));
}
}

View File

@@ -0,0 +1,7 @@
package vvpkassistant.pk.service;
import vvpkassistant.pk.model.DTO.PkResultPointsDTO;
public interface PkResultPointService {
String grantPkResultPoints(PkResultPointsDTO request);
}

View File

@@ -0,0 +1,100 @@
package vvpkassistant.pk.service;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import vvpkassistant.CoinRecords.CoinRecords;
import vvpkassistant.CoinRecords.CoinRecordsDao;
import vvpkassistant.Tools.EpochSecondProvider;
import vvpkassistant.User.mapper.UserDao;
import vvpkassistant.common.ErrorCode;
import vvpkassistant.config.FunctionConfigProvider;
import vvpkassistant.exception.BusinessException;
import vvpkassistant.pk.model.DTO.PkResultPointsDTO;
@Service
public class PkResultPointServiceImpl implements PkResultPointService {
private static final String PK_WIN_COIN_CONFIG_NAME = "PK胜利增加积分";
private static final String PK_LOSE_COIN_CONFIG_NAME = "PK失败增加积分";
private static final String PK_DRAW_COIN_CONFIG_NAME = "PK平局增加积分";
private static final int COIN_RECORD_ADD = 1;
private final UserDao userDao;
private final CoinRecordsDao coinRecordsDao;
private final FunctionConfigProvider functionConfigProvider;
private final EpochSecondProvider epochSecondProvider;
public PkResultPointServiceImpl(
UserDao userDao,
CoinRecordsDao coinRecordsDao,
FunctionConfigProvider functionConfigProvider,
EpochSecondProvider epochSecondProvider
) {
this.userDao = userDao;
this.coinRecordsDao = coinRecordsDao;
this.functionConfigProvider = functionConfigProvider;
this.epochSecondProvider = epochSecondProvider;
}
@Override
@Transactional(rollbackFor = Exception.class)
public String grantPkResultPoints(PkResultPointsDTO request) {
validateRequest(request);
int now = (int) epochSecondProvider.nowEpochSecond();
if (isDraw(request)) {
return grantDrawPoints(request, now);
}
int winPoints = parsePositivePoints(PK_WIN_COIN_CONFIG_NAME);
int losePoints = parsePositivePoints(PK_LOSE_COIN_CONFIG_NAME);
grantPoints(request.getWinnerUserId(), winPoints, PK_WIN_COIN_CONFIG_NAME, now);
grantPoints(request.getLoserUserId(), losePoints, PK_LOSE_COIN_CONFIG_NAME, now);
return String.format("操作成功,胜利方增加%d积分失败方增加%d积分", winPoints, losePoints);
}
private String grantDrawPoints(PkResultPointsDTO request, int now) {
int drawPoints = parsePositivePoints(PK_DRAW_COIN_CONFIG_NAME);
grantPoints(request.getWinnerUserId(), drawPoints, PK_DRAW_COIN_CONFIG_NAME, now);
grantPoints(request.getLoserUserId(), drawPoints, PK_DRAW_COIN_CONFIG_NAME, now);
return String.format("操作成功,平局双方各增加%d积分", drawPoints);
}
private static void validateRequest(PkResultPointsDTO request) {
if (request == null || request.getWinnerUserId() == null || request.getLoserUserId() == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数不能为空");
}
if (request.getWinnerUserId().equals(request.getLoserUserId())) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "胜利者和失败者不能是同一用户");
}
}
private static boolean isDraw(PkResultPointsDTO request) {
return Boolean.TRUE.equals(request.getDraw());
}
private int parsePositivePoints(String configName) {
String value = functionConfigProvider.getValue(configName);
if (value == null) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "未配置" + configName);
}
try {
int points = Integer.parseInt(value);
if (points < 0) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, configName + "配置错误");
}
return points;
} catch (NumberFormatException e) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, configName + "配置错误");
}
}
private void grantPoints(int userId, int points, String configName, int now) {
if (userDao.increasePoints(userId, points) != 1) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "用户不存在");
}
CoinRecords coinRecords = new CoinRecords(configName, userId, points, now, COIN_RECORD_ADD);
if (coinRecordsDao.insert(coinRecords) != 1) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "积分记录写入失败");
}
}
}

View File

@@ -0,0 +1,14 @@
package vvpkassistant.tenant.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import vvpkassistant.tenant.model.SystemTenant;
/*
* @author: ziin
* @date: 2026/3/27 13:17
*/
@Mapper
public interface SystemTenantMapper extends BaseMapper<SystemTenant> {
}

View File

@@ -0,0 +1,137 @@
package vvpkassistant.tenant.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
/*
* @author: ziin
* @date: 2026/3/27 13:17
*/
/**
* 租户表
*/
@Data
@TableName("system_tenant")
public class SystemTenant {
/**
* 租户编号
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 租户名
*/
private String name;
/**
* 联系人的用户编号
*/
private Long contactUserId;
/**
* 联系人
*/
private String contactName;
/**
* 联系手机
*/
private String contactMobile;
/**
* 租户状态0正常 1停用
*/
private Byte status;
/**
* 绑定域名
*/
private String website;
/**
* 租户套餐编号
*/
private Long packageId;
/**
* 过期时间
*/
private Date expireTime;
/**
* 账号数量
*/
private Integer accountCount;
/**
* 创建者
*/
private String creator;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新者
*/
private String updater;
/**
* 更新时间
*/
private Date updateTime;
/**
* 是否删除
*/
private Boolean deleted;
/**
* 备注
*/
private String remark;
/**
* 爬虫到期时间
*/
private Date crawlExpireTime;
/**
* AI 到期时间
*/
private Date aiExpireTime;
/**
* 大哥过期时间
*/
private Date brotherExpireTime;
/**
* 上级租户 Id
*/
private Long parentId;
/**
* 租户类型:代理,用户
*/
private String tenantType;
/**
* 代理级别
*/
private Integer tenantLevel;
/**
* 初始用户
*/
private String initialUser;
}

View File

@@ -3,14 +3,13 @@
spring: spring:
datasource: datasource:
driver-class-name: com.mysql.cj.jdbc.Driver driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://47.79.98.113:3326/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true
username: root username: root
password: niu995228 password: wfn53400
url: jdbc:mysql://49.235.115.212:3336/vv_assistant
redis: redis:
database: 1 database: 1
host: localhost host: 47.79.98.113
port: 16379 port: 16379
timeout: 5000 timeout: 5000
password: ezyPM2UQkPO8O6i8s9 password: ezyPM2UQkPO8O6i8s9

View File

@@ -58,4 +58,4 @@ dromara:
domain: https://vv-1317974657.cos.ap-shanghai.myqcloud.com # 访问域名,注意“/”结尾例如https://abc.cos.ap-nanjing.myqcloud.com/ domain: https://vv-1317974657.cos.ap-shanghai.myqcloud.com # 访问域名,注意“/”结尾例如https://abc.cos.ap-nanjing.myqcloud.com/
base-path: /headerIcon/ # 基础路径 base-path: /headerIcon/ # 基础路径
IM-secretKey: 04452c3231ae4fe5 IM-secretKey: 7a2b9359f06646f9

View File

@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="DEBUG">
<Appenders>
<!-- 控制台输出 -->
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
<!-- 文件输出 -->
<File name="FileAppender" fileName="logs/app.log">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n"/>
</File>
</Appenders>
<Loggers>
<Root level="DEBUG">
<AppenderRef ref="Console"/>
<AppenderRef ref="FileAppender"/>
</Root>
</Loggers>
</Configuration>

View File

@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds">
<contextName>vvPkAssistant</contextName>
<property name="PATTERN_DEFAULT"
value="%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} | %highlight(${LOG_LEVEL_PATTERN:-%5p} ${PID:- }) | %boldYellow(%thread) %boldGreen(%-40.40logger{39}) | %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%ex}"/>
<property name="FILE_PATTERN"
value="%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} | ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } | %thread %-40.40logger{39} | %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%ex}"/>
<property name="LOG_FILE_PATH" value="${LOG_FILE:-${user.dir}/logs/vvpkassistant}"/>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${PATTERN_DEFAULT}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_FILE_PATH}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOGBACK_ROLLINGPOLICY_FILE_NAME_PATTERN:-${LOG_FILE_PATH}.%d{yyyy-MM-dd}.%i.gz}</fileNamePattern>
<maxHistory>${LOGBACK_ROLLINGPOLICY_MAX_HISTORY:-30}</maxHistory>
<maxFileSize>${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE:-10MB}</maxFileSize>
</rollingPolicy>
<encoder>
<pattern>${FILE_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<springProfile name="local,dev">
<root level="INFO">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
</root>
<logger name="vvpkassistant" level="DEBUG" additivity="false">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
</logger>
<logger name="com.baomidou.mybatisplus" level="INFO"/>
</springProfile>
<springProfile name="prod">
<root level="INFO">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
</root>
<logger name="vvpkassistant" level="INFO" additivity="false">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
</logger>
<logger name="org.springframework" level="WARN"/>
<logger name="com.baomidou.mybatisplus" level="WARN"/>
<logger name="org.redisson" level="WARN"/>
</springProfile>
</configuration>

View File

@@ -0,0 +1,206 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="vvpkassistant.item.PkItemMapper">
<resultMap id="BaseResultMap" type="vvpkassistant.item.model.PkItem">
<!--@mbg.generated-->
<!--@Table pk_item-->
<id column="id" jdbcType="BIGINT" property="id" />
<result column="item_name" jdbcType="VARCHAR" property="itemName" />
<result column="item_price" jdbcType="INTEGER" property="itemPrice" />
<result column="item_desc" jdbcType="VARCHAR" property="itemDesc" />
<result column="item_function" jdbcType="VARCHAR" property="itemFunction" />
<result column="item_duration" jdbcType="INTEGER" property="itemDuration" />
<result column="item_status" jdbcType="INTEGER" property="itemStatus" />
<result column="remark" jdbcType="VARCHAR" property="remark" />
<result column="creator" jdbcType="VARCHAR" property="creator" />
<result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
<result column="updater" jdbcType="VARCHAR" property="updater" />
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
<result column="deleted" jdbcType="BIT" property="deleted" />
</resultMap>
<sql id="Base_Column_List">
<!--@mbg.generated-->
id, item_name, item_price, item_desc, item_function, item_duration, item_status,
remark, creator, create_time, updater, update_time, deleted
</sql>
<select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
<!--@mbg.generated-->
select
<include refid="Base_Column_List" />
from pk_item
where id = #{id,jdbcType=BIGINT}
</select>
<select id="selectItemList" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from pk_item
where deleted = 0
and
item_status = 0
order by id desc
</select>
<delete id="deleteByPrimaryKey" parameterType="java.lang.Long">
<!--@mbg.generated-->
delete from pk_item
where id = #{id,jdbcType=BIGINT}
</delete>
<insert id="insert" parameterType="vvpkassistant.item.model.PkItem">
<!--@mbg.generated-->
insert into pk_item (id, item_name, item_price,
item_desc, item_function, item_duration,
item_status, remark, creator,
create_time, updater, update_time,
deleted)
values (#{id,jdbcType=BIGINT}, #{itemName,jdbcType=VARCHAR}, #{itemPrice,jdbcType=INTEGER},
#{itemDesc,jdbcType=VARCHAR}, #{itemFunction,jdbcType=VARCHAR}, #{itemDuration,jdbcType=INTEGER},
#{itemStatus,jdbcType=INTEGER}, #{remark,jdbcType=VARCHAR}, #{creator,jdbcType=VARCHAR},
#{createTime,jdbcType=TIMESTAMP}, #{updater,jdbcType=VARCHAR}, #{updateTime,jdbcType=TIMESTAMP},
#{deleted,jdbcType=BIT})
</insert>
<insert id="insertSelective" parameterType="vvpkassistant.item.model.PkItem">
<!--@mbg.generated-->
insert into pk_item
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">
id,
</if>
<if test="itemName != null">
item_name,
</if>
<if test="itemPrice != null">
item_price,
</if>
<if test="itemDesc != null">
item_desc,
</if>
<if test="itemFunction != null">
item_function,
</if>
<if test="itemDuration != null">
item_duration,
</if>
<if test="itemStatus != null">
item_status,
</if>
<if test="remark != null">
remark,
</if>
<if test="creator != null">
creator,
</if>
<if test="createTime != null">
create_time,
</if>
<if test="updater != null">
updater,
</if>
<if test="updateTime != null">
update_time,
</if>
<if test="deleted != null">
deleted,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="id != null">
#{id,jdbcType=BIGINT},
</if>
<if test="itemName != null">
#{itemName,jdbcType=VARCHAR},
</if>
<if test="itemPrice != null">
#{itemPrice,jdbcType=INTEGER},
</if>
<if test="itemDesc != null">
#{itemDesc,jdbcType=VARCHAR},
</if>
<if test="itemFunction != null">
#{itemFunction,jdbcType=VARCHAR},
</if>
<if test="itemDuration != null">
#{itemDuration,jdbcType=INTEGER},
</if>
<if test="itemStatus != null">
#{itemStatus,jdbcType=INTEGER},
</if>
<if test="remark != null">
#{remark,jdbcType=VARCHAR},
</if>
<if test="creator != null">
#{creator,jdbcType=VARCHAR},
</if>
<if test="createTime != null">
#{createTime,jdbcType=TIMESTAMP},
</if>
<if test="updater != null">
#{updater,jdbcType=VARCHAR},
</if>
<if test="updateTime != null">
#{updateTime,jdbcType=TIMESTAMP},
</if>
<if test="deleted != null">
#{deleted,jdbcType=BIT},
</if>
</trim>
</insert>
<update id="updateByPrimaryKeySelective" parameterType="vvpkassistant.item.model.PkItem">
<!--@mbg.generated-->
update pk_item
<set>
<if test="itemName != null">
item_name = #{itemName,jdbcType=VARCHAR},
</if>
<if test="itemPrice != null">
item_price = #{itemPrice,jdbcType=INTEGER},
</if>
<if test="itemDesc != null">
item_desc = #{itemDesc,jdbcType=VARCHAR},
</if>
<if test="itemFunction != null">
item_function = #{itemFunction,jdbcType=VARCHAR},
</if>
<if test="itemDuration != null">
item_duration = #{itemDuration,jdbcType=INTEGER},
</if>
<if test="itemStatus != null">
item_status = #{itemStatus,jdbcType=INTEGER},
</if>
<if test="remark != null">
remark = #{remark,jdbcType=VARCHAR},
</if>
<if test="creator != null">
creator = #{creator,jdbcType=VARCHAR},
</if>
<if test="createTime != null">
create_time = #{createTime,jdbcType=TIMESTAMP},
</if>
<if test="updater != null">
updater = #{updater,jdbcType=VARCHAR},
</if>
<if test="updateTime != null">
update_time = #{updateTime,jdbcType=TIMESTAMP},
</if>
<if test="deleted != null">
deleted = #{deleted,jdbcType=BIT},
</if>
</set>
where id = #{id,jdbcType=BIGINT}
</update>
<update id="updateByPrimaryKey" parameterType="vvpkassistant.item.model.PkItem">
<!--@mbg.generated-->
update pk_item
set item_name = #{itemName,jdbcType=VARCHAR},
item_price = #{itemPrice,jdbcType=INTEGER},
item_desc = #{itemDesc,jdbcType=VARCHAR},
item_function = #{itemFunction,jdbcType=VARCHAR},
item_duration = #{itemDuration,jdbcType=INTEGER},
item_status = #{itemStatus,jdbcType=INTEGER},
remark = #{remark,jdbcType=VARCHAR},
creator = #{creator,jdbcType=VARCHAR},
create_time = #{createTime,jdbcType=TIMESTAMP},
updater = #{updater,jdbcType=VARCHAR},
update_time = #{updateTime,jdbcType=TIMESTAMP},
deleted = #{deleted,jdbcType=BIT}
where id = #{id,jdbcType=BIGINT}
</update>
</mapper>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="vvpkassistant.iterm_recoder.mapper.PkItemRecoderMapper">
<resultMap id="BaseResultMap" type="vvpkassistant.iterm_recoder.model.PkItemRecoder">
<!--@mbg.generated-->
<!--@Table pk_item_recoder-->
<id column="Id" jdbcType="BIGINT" property="id" />
<result column="user_id" jdbcType="BIGINT" property="userId" />
<result column="item_id" jdbcType="VARCHAR" property="itemId" />
<result column="creator" jdbcType="VARCHAR" property="creator" />
<result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
<result column="updater" jdbcType="VARCHAR" property="updater" />
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
<result column="deleted" jdbcType="BIT" property="deleted" />
</resultMap>
<sql id="Base_Column_List">
<!--@mbg.generated-->
Id, user_Id, item_id, creator, create_time, updater, update_time, deleted
</sql>
</mapper>

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="vvpkassistant.tenant.mapper.SystemTenantMapper">
<resultMap id="BaseResultMap" type="vvpkassistant.tenant.model.SystemTenant">
<!--@mbg.generated-->
<!--@Table system_tenant-->
<id column="id" jdbcType="BIGINT" property="id" />
<result column="name" jdbcType="VARCHAR" property="name" />
<result column="contact_user_id" jdbcType="BIGINT" property="contactUserId" />
<result column="contact_name" jdbcType="VARCHAR" property="contactName" />
<result column="contact_mobile" jdbcType="VARCHAR" property="contactMobile" />
<result column="status" jdbcType="TINYINT" property="status" />
<result column="website" jdbcType="VARCHAR" property="website" />
<result column="package_id" jdbcType="BIGINT" property="packageId" />
<result column="expire_time" jdbcType="TIMESTAMP" property="expireTime" />
<result column="account_count" jdbcType="INTEGER" property="accountCount" />
<result column="creator" jdbcType="VARCHAR" property="creator" />
<result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
<result column="updater" jdbcType="VARCHAR" property="updater" />
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
<result column="deleted" jdbcType="BIT" property="deleted" />
<result column="remark" jdbcType="VARCHAR" property="remark" />
<result column="crawl_expire_time" jdbcType="TIMESTAMP" property="crawlExpireTime" />
<result column="ai_expire_time" jdbcType="TIMESTAMP" property="aiExpireTime" />
<result column="brother_expire_time" jdbcType="TIMESTAMP" property="brotherExpireTime" />
<result column="parent_Id" jdbcType="BIGINT" property="parentId" />
<result column="tenant_type" jdbcType="VARCHAR" property="tenantType" />
<result column="tenant_level" jdbcType="INTEGER" property="tenantLevel" />
<result column="initial_user" jdbcType="VARCHAR" property="initialUser" />
</resultMap>
<sql id="Base_Column_List">
<!--@mbg.generated-->
id, `name`, contact_user_id, contact_name, contact_mobile, `status`, website, package_id,
expire_time, account_count, creator, create_time, updater, update_time, deleted,
remark, crawl_expire_time, ai_expire_time, brother_expire_time, parent_Id, tenant_type,
tenant_level, initial_user
</sql>
</mapper>

View File

@@ -0,0 +1,119 @@
package vvpkassistant.controller;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import vvpkassistant.Data.ResponseData;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
class ControllerMapToDtoContractTests {
@Test
void shouldKeepAllFormerMapEndpointsAndReturnTypes() {
Map<String, Class<?>> expected = new LinkedHashMap<>();
expected.put("user/inputUserInfo", ResponseData.class);
expected.put("user/loginWithPhoneNumber", ResponseData.class);
expected.put("user/queryMyAllPkData", ResponseData.class);
expected.put("user/handlePkInfo", ResponseData.class);
expected.put("user/pkRecordDetail", ResponseData.class);
expected.put("user/pinToTop", ResponseData.class);
expected.put("user/cancelPin", ResponseData.class);
expected.put("user/pointsDetail", ResponseData.class);
expected.put("pk/pkList", ResponseData.class);
expected.put("pk/queryMyCanUsePkData", ResponseData.class);
expected.put("pk/pkInfoDetail", ResponseData.class);
expected.put("pk/deletePkDataWithId", ResponseData.class);
expected.put("pk/fetchDetailPkDataWithId", ResponseData.class);
expected.put("pk/listUninvitedPublishedAnchorsByUserId", ResponseData.class);
expected.put("anchor/list", ResponseData.class);
expected.put("anchor/deleteMyAnchor", ResponseData.class);
expected.put("systemMessage/list", ResponseData.class);
expected.put("chat/receiveImMessage", Map.class);
Map<String, Class<?>> actual = new HashMap<>();
Class<?>[] controllers = new Class<?>[]{
UserController.class,
PkController.class,
AnchorsController.class,
SystemMessageController.class,
ChatController.class
};
for (Class<?> controllerClass : controllers) {
RequestMapping requestMapping = controllerClass.getAnnotation(RequestMapping.class);
String basePath = requestMapping == null ? "" : firstPath(requestMapping.value(), requestMapping.path());
for (Method method : controllerClass.getDeclaredMethods()) {
PostMapping postMapping = method.getAnnotation(PostMapping.class);
if (postMapping == null) {
continue;
}
String subPath = firstPath(postMapping.value(), postMapping.path());
String fullPath = joinPath(basePath, subPath);
if (expected.containsKey(fullPath)) {
actual.put(fullPath, method.getReturnType());
}
}
}
Assertions.assertEquals(18, actual.size(), "目标端点数量不匹配");
expected.forEach((path, returnType) ->
Assertions.assertEquals(returnType, actual.get(path), "端点返回类型不一致: " + path)
);
}
@Test
void shouldNotUseRequestBodyMapInTargetControllers() {
Class<?>[] controllers = new Class<?>[]{
UserController.class,
PkController.class,
AnchorsController.class,
SystemMessageController.class,
ChatController.class
};
for (Class<?> controllerClass : controllers) {
for (Method method : controllerClass.getDeclaredMethods()) {
for (Parameter parameter : method.getParameters()) {
if (!hasRequestBody(parameter.getAnnotations())) {
continue;
}
Assertions.assertFalse(Map.class.isAssignableFrom(parameter.getType()),
"存在 @RequestBody Map 参数: " + controllerClass.getSimpleName() + "." + method.getName());
}
}
}
}
private static boolean hasRequestBody(Annotation[] annotations) {
return Arrays.stream(annotations).anyMatch(annotation -> annotation.annotationType() == RequestBody.class);
}
private static String firstPath(String[] values, String[] paths) {
if (values != null && values.length > 0 && values[0] != null && !values[0].isEmpty()) {
return values[0];
}
if (paths != null && paths.length > 0 && paths[0] != null && !paths[0].isEmpty()) {
return paths[0];
}
return "";
}
private static String joinPath(String basePath, String subPath) {
String base = basePath.startsWith("/") ? basePath.substring(1) : basePath;
String sub = subPath.startsWith("/") ? subPath.substring(1) : subPath;
if (base.isEmpty()) {
return sub;
}
if (sub.isEmpty()) {
return base;
}
return base + "/" + sub;
}
}

View File

@@ -0,0 +1,52 @@
package vvpkassistant.dao;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Select;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import vvpkassistant.User.mapper.SignInRecordDao;
import vvpkassistant.User.mapper.UserDao;
import vvpkassistant.pk.mapper.PkInfoDao;
import vvpkassistant.pk.mapper.PkRecordDao;
import vvpkassistant.pk.mapper.PkRecordDetailDao;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
class DaoLambdaMigrationContractTests {
@Test
void shouldRemoveCrossTableMethodsFromUserDao() {
List<String> methodNames = Arrays.asList("findCreatedPk", "getMyGuestPkList", "signIn", "checkSignStatus");
for (Method method : UserDao.class.getMethods()) {
Assertions.assertFalse(methodNames.contains(method.getName()),
"UserDao 仍包含跨表方法: " + method.getName());
}
}
@Test
void shouldUseNonAnnotatedLambdaMethodsForMigratedDaos() throws Exception {
assertNoSelect(UserDao.class.getMethod("queryWithPhoneNumber", String.class));
assertNoSelect(PkInfoDao.class.getMethod("selectPkInfoByCondition", int.class, int.class, java.util.Map.class, long.class, long.class));
assertNoSelect(PkRecordDao.class.getMethod("pkListForToday", long.class, long.class));
assertNoSelect(PkRecordDao.class.getMethod("fetchDataFromTodayWithUserId", Integer.class, Long.class));
assertNoSelect(PkRecordDao.class.getMethod("getPendingInvitations", String.class, String.class));
assertNoSelect(PkRecordDao.class.getMethod("singleRecord", Integer.class));
assertNoSelect(PkRecordDao.class.getMethod("findCreatedPk", Integer.class, Integer.class, Integer.class));
assertNoSelect(PkRecordDao.class.getMethod("getMyGuestPkList", Integer.class, Integer.class, Integer.class));
assertNoSelect(PkRecordDetailDao.class.getMethod("queryDetail", Integer.class));
}
@Test
void shouldUseSignInRecordDaoForSignInChain() throws Exception {
Method signIn = SignInRecordDao.class.getMethod("signIn", int.class);
Method checkStatus = SignInRecordDao.class.getMethod("checkSignStatus", int.class);
Assertions.assertNull(signIn.getAnnotation(Insert.class), "SignInRecordDao.signIn 不应使用注解SQL");
Assertions.assertNull(checkStatus.getAnnotation(Select.class), "SignInRecordDao.checkSignStatus 不应使用注解SQL");
}
private static void assertNoSelect(Method method) {
Assertions.assertNull(method.getAnnotation(Select.class), "方法不应保留 @Select: " + method.getName());
}
}

View File

@@ -0,0 +1,41 @@
package vvpkassistant.e2e;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.List;
class CoreFlowRegressionCaseCatalogTests {
private static final List<String> CORE_CASES = Arrays.asList(
"/pk/pkList condition为空 userId为空",
"/pk/pkList condition有值 userId有值",
"/pk/pkInfoDetail from=1 可见主播ID",
"/pk/pkInfoDetail from=1 隐藏主播ID",
"/pk/deletePkDataWithId 置顶中删除失败",
"/pk/createPkRecord 首次邀请成功",
"/pk/createPkRecord 重复未处理邀请失败",
"/user/signIn 首次签到成功",
"/user/signIn 当日重复签到失败",
"/user/handlePkInfo type=1/type=2 分支查询"
);
private static final List<String> EDGE_CASES = Arrays.asList(
"重复邀请并发提交",
"重复签到并发提交",
"置顶到期边界(临界秒)"
);
@Test
void shouldCoverAtLeastTenCoreCases() {
Assertions.assertTrue(CORE_CASES.size() >= 10, "核心链路回归用例不足10条");
}
@Test
void shouldIncludeConcurrencyAndBoundaryCases() {
Assertions.assertTrue(EDGE_CASES.contains("重复邀请并发提交"));
Assertions.assertTrue(EDGE_CASES.contains("重复签到并发提交"));
Assertions.assertTrue(EDGE_CASES.contains("置顶到期边界(临界秒)"));
}
}

View File

@@ -0,0 +1,33 @@
package vvpkassistant.e2e;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.List;
class NonRefactorSmokeCaseCatalogTests {
private static final List<String> SMOKE_CASES = Arrays.asList(
"/functionConfig/getAllConfig 成功",
"/functionConfig/getAllConfig 参数异常",
"/functionConfig/updateConfigValue 成功",
"/functionConfig/updateConfigValue 参数异常",
"/file/upload 成功",
"/file/upload 空文件异常",
"/otp/getOTP 成功",
"/otp/getOTP 密钥异常"
);
@Test
void shouldCoverTargetSmokeCases() {
Assertions.assertTrue(SMOKE_CASES.size() >= 8, "非改造模块冒烟用例不足");
}
@Test
void shouldContainRequiredModules() {
Assertions.assertTrue(SMOKE_CASES.stream().anyMatch(caseName -> caseName.startsWith("/functionConfig/")));
Assertions.assertTrue(SMOKE_CASES.stream().anyMatch(caseName -> caseName.startsWith("/file/")));
Assertions.assertTrue(SMOKE_CASES.stream().anyMatch(caseName -> caseName.startsWith("/otp/")));
}
}

View File

@@ -0,0 +1,109 @@
package vvpkassistant.pk.service;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import vvpkassistant.CoinRecords.CoinRecordsDao;
import vvpkassistant.Tools.EpochSecondProvider;
import vvpkassistant.User.mapper.UserDao;
import vvpkassistant.User.model.UserModel;
import vvpkassistant.common.ErrorCode;
import vvpkassistant.config.FunctionConfigProvider;
import vvpkassistant.exception.BusinessException;
import vvpkassistant.pk.mapper.PkInfoDao;
import vvpkassistant.pk.model.PkInfoModel;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
class PkPinServiceImplTests {
@Test
void shouldFailWhenExpireTimeNotInFuture() {
PkPinServiceImpl service = newServiceWithNow(100L);
BusinessException ex = Assertions.assertThrows(BusinessException.class,
() -> service.pinToTop(1, 10, 100));
Assertions.assertEquals(ErrorCode.PARAMS_ERROR.getCode(), ex.getCode());
}
@Test
void shouldFailWhenPointsNotEnoughEvenIfLessThanOneHour() {
long now = 1_000L;
PkInfoDao pkInfoDao = mock(PkInfoDao.class);
UserDao userDao = mock(UserDao.class);
CoinRecordsDao coinRecordsDao = mock(CoinRecordsDao.class);
FunctionConfigProvider functionConfigProvider = mock(FunctionConfigProvider.class);
EpochSecondProvider epochSecondProvider = () -> now;
when(functionConfigProvider.getValue("置顶扣除积分")).thenReturn("10");
PkInfoModel pkInfoModel = new PkInfoModel();
pkInfoModel.setId(10);
pkInfoModel.setSenderId(1);
when(pkInfoDao.selectById(10)).thenReturn(pkInfoModel);
when(userDao.decreasePointsIfEnough(1, 10)).thenReturn(0);
UserModel userModel = new UserModel();
userModel.setId(1);
userModel.setPoints(9);
when(userDao.selectById(1)).thenReturn(userModel);
PkPinServiceImpl service = new PkPinServiceImpl(
pkInfoDao, userDao, coinRecordsDao, functionConfigProvider, epochSecondProvider
);
int pinExpireTime = (int) (now + 1);
BusinessException ex = Assertions.assertThrows(BusinessException.class,
() -> service.pinToTop(1, 10, pinExpireTime));
Assertions.assertTrue(ex.getMessage().contains("积分不足"));
verify(pkInfoDao, never()).updateById(any());
verify(coinRecordsDao, never()).insert(any());
}
@Test
void shouldSucceedAndChargeOneHourWhenJustOneSecond() {
long now = 2_000L;
PkInfoDao pkInfoDao = mock(PkInfoDao.class);
UserDao userDao = mock(UserDao.class);
CoinRecordsDao coinRecordsDao = mock(CoinRecordsDao.class);
FunctionConfigProvider functionConfigProvider = mock(FunctionConfigProvider.class);
EpochSecondProvider epochSecondProvider = () -> now;
when(functionConfigProvider.getValue("置顶扣除积分")).thenReturn("10");
PkInfoModel pkInfoModel = new PkInfoModel();
pkInfoModel.setId(10);
pkInfoModel.setSenderId(1);
when(pkInfoDao.selectById(10)).thenReturn(pkInfoModel);
when(userDao.decreasePointsIfEnough(1, 10)).thenReturn(1);
when(pkInfoDao.updateById(any())).thenReturn(1);
when(coinRecordsDao.insert(any())).thenReturn(1);
PkPinServiceImpl service = new PkPinServiceImpl(
pkInfoDao, userDao, coinRecordsDao, functionConfigProvider, epochSecondProvider
);
int pinExpireTime = (int) (now + 1);
String msg = service.pinToTop(1, 10, pinExpireTime);
Assertions.assertTrue(msg.contains("扣除10积分"));
ArgumentCaptor<PkInfoModel> captor = ArgumentCaptor.forClass(PkInfoModel.class);
verify(pkInfoDao).updateById(captor.capture());
Assertions.assertEquals(Integer.valueOf(pinExpireTime), captor.getValue().getPinExpireTime());
Assertions.assertEquals(Integer.valueOf((int) now), captor.getValue().getPinCreateTime());
}
private static PkPinServiceImpl newServiceWithNow(long now) {
PkInfoDao pkInfoDao = mock(PkInfoDao.class);
UserDao userDao = mock(UserDao.class);
CoinRecordsDao coinRecordsDao = mock(CoinRecordsDao.class);
FunctionConfigProvider functionConfigProvider = mock(FunctionConfigProvider.class);
EpochSecondProvider epochSecondProvider = () -> now;
return new PkPinServiceImpl(pkInfoDao, userDao, coinRecordsDao, functionConfigProvider, epochSecondProvider);
}
}

View File

@@ -0,0 +1,110 @@
package vvpkassistant.pk.service;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import vvpkassistant.CoinRecords.CoinRecords;
import vvpkassistant.CoinRecords.CoinRecordsDao;
import vvpkassistant.Tools.EpochSecondProvider;
import vvpkassistant.User.mapper.UserDao;
import vvpkassistant.common.ErrorCode;
import vvpkassistant.config.FunctionConfigProvider;
import vvpkassistant.exception.BusinessException;
import vvpkassistant.pk.model.DTO.PkResultPointsDTO;
import static org.mockito.Mockito.*;
class PkResultPointServiceImplTests {
@Test
void shouldGrantWinAndLosePointsTogether() {
UserDao userDao = mock(UserDao.class);
CoinRecordsDao coinRecordsDao = mock(CoinRecordsDao.class);
FunctionConfigProvider functionConfigProvider = mock(FunctionConfigProvider.class);
EpochSecondProvider epochSecondProvider = () -> 1_234L;
when(functionConfigProvider.getValue("PK胜利增加积分")).thenReturn("8");
when(functionConfigProvider.getValue("PK失败增加积分")).thenReturn("3");
when(userDao.increasePoints(1001, 8)).thenReturn(1);
when(userDao.increasePoints(1002, 3)).thenReturn(1);
when(coinRecordsDao.insert(any())).thenReturn(1);
PkResultPointServiceImpl service = new PkResultPointServiceImpl(
userDao, coinRecordsDao, functionConfigProvider, epochSecondProvider
);
PkResultPointsDTO request = new PkResultPointsDTO();
request.setWinnerUserId(1001);
request.setLoserUserId(1002);
String result = service.grantPkResultPoints(request);
Assertions.assertEquals("操作成功胜利方增加8积分失败方增加3积分", result);
ArgumentCaptor<CoinRecords> captor = ArgumentCaptor.forClass(CoinRecords.class);
verify(coinRecordsDao, times(2)).insert(captor.capture());
Assertions.assertEquals("PK胜利增加积分", captor.getAllValues().get(0).getInfo());
Assertions.assertEquals(Integer.valueOf(1001), captor.getAllValues().get(0).getUserId());
Assertions.assertEquals(Integer.valueOf(8), captor.getAllValues().get(0).getNumber());
Assertions.assertEquals("PK失败增加积分", captor.getAllValues().get(1).getInfo());
Assertions.assertEquals(Integer.valueOf(1002), captor.getAllValues().get(1).getUserId());
Assertions.assertEquals(Integer.valueOf(3), captor.getAllValues().get(1).getNumber());
Assertions.assertEquals(Integer.valueOf(1234), captor.getAllValues().get(0).getTime());
Assertions.assertEquals(Integer.valueOf(1234), captor.getAllValues().get(1).getTime());
}
@Test
void shouldGrantDrawPointsToBothUsers() {
UserDao userDao = mock(UserDao.class);
CoinRecordsDao coinRecordsDao = mock(CoinRecordsDao.class);
FunctionConfigProvider functionConfigProvider = mock(FunctionConfigProvider.class);
EpochSecondProvider epochSecondProvider = () -> 1_234L;
when(functionConfigProvider.getValue("PK平局增加积分")).thenReturn("5");
when(userDao.increasePoints(1001, 5)).thenReturn(1);
when(userDao.increasePoints(1002, 5)).thenReturn(1);
when(coinRecordsDao.insert(any())).thenReturn(1);
PkResultPointServiceImpl service = new PkResultPointServiceImpl(
userDao, coinRecordsDao, functionConfigProvider, epochSecondProvider
);
PkResultPointsDTO request = new PkResultPointsDTO();
request.setWinnerUserId(1001);
request.setLoserUserId(1002);
request.setDraw(true);
String result = service.grantPkResultPoints(request);
Assertions.assertEquals("操作成功平局双方各增加5积分", result);
ArgumentCaptor<CoinRecords> captor = ArgumentCaptor.forClass(CoinRecords.class);
verify(coinRecordsDao, times(2)).insert(captor.capture());
Assertions.assertEquals("PK平局增加积分", captor.getAllValues().get(0).getInfo());
Assertions.assertEquals(Integer.valueOf(1001), captor.getAllValues().get(0).getUserId());
Assertions.assertEquals(Integer.valueOf(5), captor.getAllValues().get(0).getNumber());
Assertions.assertEquals("PK平局增加积分", captor.getAllValues().get(1).getInfo());
Assertions.assertEquals(Integer.valueOf(1002), captor.getAllValues().get(1).getUserId());
Assertions.assertEquals(Integer.valueOf(5), captor.getAllValues().get(1).getNumber());
Assertions.assertEquals(Integer.valueOf(1234), captor.getAllValues().get(0).getTime());
Assertions.assertEquals(Integer.valueOf(1234), captor.getAllValues().get(1).getTime());
}
@Test
void shouldFailWhenWinnerAndLoserAreSameUser() {
UserDao userDao = mock(UserDao.class);
CoinRecordsDao coinRecordsDao = mock(CoinRecordsDao.class);
FunctionConfigProvider functionConfigProvider = mock(FunctionConfigProvider.class);
EpochSecondProvider epochSecondProvider = () -> 1_234L;
PkResultPointServiceImpl service = new PkResultPointServiceImpl(
userDao, coinRecordsDao, functionConfigProvider, epochSecondProvider
);
PkResultPointsDTO request = new PkResultPointsDTO();
request.setWinnerUserId(1001);
request.setLoserUserId(1001);
BusinessException ex = Assertions.assertThrows(BusinessException.class,
() -> service.grantPkResultPoints(request));
Assertions.assertEquals(ErrorCode.PARAMS_ERROR.getCode(), ex.getCode());
verifyNoInteractions(functionConfigProvider, coinRecordsDao);
verify(userDao, never()).increasePoints(anyInt(), anyInt());
}
}