diff --git a/src/main/java/com/rj/controller/LbDepartmentUserController.java b/src/main/java/com/rj/controller/LbDepartmentUserController.java index 9b218fc..eaa52bc 100644 --- a/src/main/java/com/rj/controller/LbDepartmentUserController.java +++ b/src/main/java/com/rj/controller/LbDepartmentUserController.java @@ -169,7 +169,7 @@ public class LbDepartmentUserController { @PostMapping("/exportUsers") @Operation(summary = "导出指定用户Excel", - description = "按租户ID与用户ID列表导出:用户ID、用户名、手机号、加入时间、最早/最晚buy_time、累计交易金额") + description = "按租户ID与用户ID列表导出:用户ID、用户名、手机号、加入时间、抢单数量、当天买单量、最早/最晚buy_time、累计交易金额") public void exportUsers( @Parameter(description = "租户ID", required = true) @RequestParam String tenantId, @Parameter(description = "用户ID列表", required = true) @RequestBody List userIds, diff --git a/src/main/java/com/rj/mapper/LbOrderRowMapper.java b/src/main/java/com/rj/mapper/LbOrderRowMapper.java index d9b347a..8ee64a1 100644 --- a/src/main/java/com/rj/mapper/LbOrderRowMapper.java +++ b/src/main/java/com/rj/mapper/LbOrderRowMapper.java @@ -41,4 +41,12 @@ public interface LbOrderRowMapper extends BaseMapper { @Param("buyerIds") List buyerIds, @Param("buyStartTime") String buyStartTime, @Param("buyEndTime") String buyEndTime); + + /** + * 按租户、买家ID列表和日期查询每个买家的当天订单数。 + */ + List> selectTodayOrderCountByBuyerIds(@Param("tenantId") String tenantId, + @Param("buyerIds") List buyerIds, + @Param("startTime") String startTime, + @Param("endTime") String endTime); } diff --git a/src/main/java/com/rj/service/impl/LbDepartmentUserServiceImpl.java b/src/main/java/com/rj/service/impl/LbDepartmentUserServiceImpl.java index d6fd90b..4259997 100644 --- a/src/main/java/com/rj/service/impl/LbDepartmentUserServiceImpl.java +++ b/src/main/java/com/rj/service/impl/LbDepartmentUserServiceImpl.java @@ -711,6 +711,30 @@ public class LbDepartmentUserServiceImpl extends ServiceImpl(userById.keySet()); + // 计算每个用户当天买单数 + Map todayBuyCountByUserId = new HashMap<>(); + if (!visibleUserIds.isEmpty()) { + List buyerIds = new ArrayList<>(visibleUserIds.size()); + for (String userId : visibleUserIds) { + try { + buyerIds.add(Long.parseLong(userId)); + } catch (NumberFormatException ignored) { + } + } + if (!buyerIds.isEmpty()) { + // buy_time 是字符串,可能为 "yyyy-MM-dd" 或 "yyyy-MM-dd HH:mm:ss" + String todayStart = LocalDate.now().format(ORDER_DAY_FMT); + String todayEnd = LocalDate.now().atTime(23, 59, 59).format(ORDER_DAY_TIME_FMT); + List> countResults = lbOrderRowMapper.selectTodayOrderCountByBuyerIds( + tenantIdTrim, buyerIds, todayStart, todayEnd); + for (Map row : countResults) { + Long buyerId = ((Number) row.get("buyerId")).longValue(); + int orderCount = ((Number) row.get("orderCount")).intValue(); + todayBuyCountByUserId.put(String.valueOf(buyerId), orderCount); + } + } + } + Map> nodeByUserId = new LinkedHashMap<>(); Map parentByUserId = new HashMap<>(); for (String userId : visibleUserIds) { @@ -718,12 +742,14 @@ public class LbDepartmentUserServiceImpl extends ServiceImpl node = toLbUserTreeNode( lbUser, joinDateByUserId.get(userId), lastTradeDateByUserId.get(userId), lastBuyAmtByUserId.get(userId), - isActiveByUserId.getOrDefault(userId, 0))); + isActiveByUserId.getOrDefault(userId, 0)); + node.put("todayBuyCount", todayBuyCountByUserId.getOrDefault(userId, 0)); + nodeByUserId.put(userId, node); String parentUserId = resolveLbUserParentId(lbUser, userById); if (parentUserId != null && visibleUserIds.contains(parentUserId)) { @@ -1036,6 +1062,7 @@ public class LbDepartmentUserServiceImpl extends ServiceImpl todayOrderCountByUserId = new HashMap<>(); + if (!buyerIds.isEmpty()) { + // buy_time 是字符串,可能为 "yyyy-MM-dd" 或 "yyyy-MM-dd HH:mm:ss"。 + // 起始用日期格式,兼容两种存储:datetime 在前缀相同时更长更大,仍满足 >=。 + String todayStart = LocalDate.now().format(ORDER_DAY_FMT); + String todayEnd = LocalDate.now().atTime(23, 59, 59).format(ORDER_DAY_TIME_FMT); + List> todayOrderCountResults = lbOrderRowMapper.selectTodayOrderCountByBuyerIds( + tenantIdTrim, buyerIds, todayStart, todayEnd); + + Map userIdByBuyerId = new HashMap<>(); + for (Map.Entry entry : buyerIdByUserId.entrySet()) { + userIdByBuyerId.put(entry.getValue(), entry.getKey()); + } + + for (Map row : todayOrderCountResults) { + Long buyerId = ((Number) row.get("buyerId")).longValue(); + int orderCount = ((Number) row.get("orderCount")).intValue(); + String userId = userIdByBuyerId.get(buyerId); + if (userId != null) { + todayOrderCountByUserId.put(userId, orderCount); + } + } + } + try (Workbook workbook = new XSSFWorkbook()) { Sheet sheet = workbook.createSheet("部门用户"); @@ -1246,7 +1297,7 @@ public class LbDepartmentUserServiceImpl extends ServiceImpl + + diff --git a/src/main/sql/商贸公司/d b/src/main/sql/商贸公司/d new file mode 100644 index 0000000..e69de29 diff --git a/src/main/sql/商贸公司/hxrd/application-dev.yml b/src/main/sql/商贸公司/hxrd/application-dev.yml new file mode 100644 index 0000000..ec08332 --- /dev/null +++ b/src/main/sql/商贸公司/hxrd/application-dev.yml @@ -0,0 +1,188 @@ +# Sql日志 +mybatis-plus: + configuration: + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + +#配置数据源 +spring: + redis: + #数据库索引 + database: ${REDIS_DB:1} + host: ${REDIS_HOST:123.57.153.32} + port: ${REDIS_PORT:16379} + password: ${REDIS_PWD:cst.redis7.pwd.HXRD.123} + #连接超时时间 + timeout: 5000 + datasource: + druid: + db-type: com.alibaba.druid.pool.DruidDataSource + driverClassName: net.sf.log4jdbc.sql.jdbcapi.DriverSpy + url: jdbc:log4jdbc:mysql://${DB_HOST:47.93.238.247}:${DB_PORT:13316}/${DB_NAME:business_shop}?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false + username: ${DB_USER:root} + password: ${DB_PWD:trade@123#} + # 初始连接数 + initial-size: 5 + # 最小连接数 + min-idle: 15 + # 最大连接数 + max-active: 30 + # 超时时间(以秒数为单位) + remove-abandoned-timeout: 180 + # 获取连接超时时间 + max-wait: 3000 + # 连接有效性检测时间 + time-between-eviction-runs-millis: 60000 + # 连接在池中最小生存的时间 + min-evictable-idle-time-millis: 300000 + # 连接在池中最大生存的时间 + max-evictable-idle-time-millis: 900000 + # 指明连接是否被空闲连接回收器(如果有)进行检验.如果检测失败,则连接将被从池中去除 + test-while-idle: true + # 指明是否在从池中取出连接前进行检验,如果检验失败, 则从池中去除连接并尝试取出另一个 + test-on-borrow: true + # 是否在归还到池中前进行检验 + test-on-return: false + # 检测连接是否有效 + validation-query: select 1 + # 配置监控统计 + webStatFilter: + enabled: true + stat-view-servlet: + enabled: true + url-pattern: /druid/* + reset-enable: false + filter: + stat: + enabled: true + # 记录慢SQL + log-slow-sql: true + slow-sql-millis: 1000 + merge-sql: true + wall: + config: + multi-statement-allow: true + +# 登录相关配置 +login: + # 默认密码 + default-password: 123456 + # 是否限制单用户登录 + single-login: false + # Redis用户登录缓存配置 + user-cache: + # 存活时间/秒 + idle-time: 21600 + # 验证码 + login-code: + # 验证码类型配置 查看 LoginProperties 类 + code-type: arithmetic + # 登录图形验证码有效时间/分钟 + expiration: 2 + # 验证码高度 + width: 111 + # 验证码宽度 + height: 36 + # 内容长度 + length: 2 + # 字体名称,为空则使用默认字体 + font-name: + # 字体大小 + font-size: 25 + +#jwt +jwt: + header: Authorization + # 令牌前缀 + token-start-with: Bearer + # 必须使用最少88位的Base64对该令牌进行编码 + base64-secret: ZmQ0ZGI5NjQ0MDQwY2I4MjMxY2Y3ZmI3MjdhN2ZmMjNhODViOTg1ZGE0NTBjMGM4NDA5NzYxMjdjOWMwYWRmZTBlZjlhNGY3ZTg4Y2U3YTE1ODVkZDU5Y2Y3OGYwZWE1NzUzNWQ2YjFjZDc0NGMxZWU2MmQ3MjY1NzJmNTE0MzI= + # 令牌过期时间 此处单位/毫秒 ,默认4小时,可在此网站生成 https://www.convertworld.com/zh-hans/time/milliseconds.html + token-validity-in-seconds: 14400000 + # 在线用户key + online-key: "online-token:" + # 验证码 + code-key: "captcha-code:" + # token 续期检查时间范围(默认30分钟,单位毫秒),在token即将过期的一段时间内用户操作了,则给用户的token续期 + detect: 1800000 + # 续期时间范围,默认1小时,单位毫秒 + renew: 3600000 + +#是否允许生成代码,生产环境设置为false +generator: + enabled: true + +#是否开启 swagger-ui +swagger: + enabled: true + +#应用上下文路径(生产环境部署时使用) +app: + context-path: + +# 文件存储路径 +file: + mac: + path: ~/file/ + avatar: ~/avatar/ + contract: ~/contract/ + contractSign: ~/contract_sign/ + backup: ~/backup/ + linux: + path: /home/eladmin/file/ + avatar: /home/eladmin/avatar/ + contract: /home/eladmin/contract/ + contractSign: /home/eladmin/contract_sign/ + backup: /home/eladmin/backup/ + windows: + path: D:\eladmin\file\ + avatar: D:\eladmin\avatar\ + contract: D:\eladmin\contract\ + contractSign: D:\eladmin\contract_sign\ + backup: D:\eladmin\backup\ + # 文件大小 /M + maxSize: 10 + avatarMaxSize: 5 + +# 识别图片 +aliyun: + ocr: + access-key-id: LTAI5t6soA4eB9d1vNWscLQT + access-key-secret: qABYANrAPgO9o52CooJ9ZB9CAW84Ka + endpoint: ocr-api.cn-hangzhou.aliyuncs.com + # 短信服务(阿里云短信,需在阿里云控制台申请签名和模板后填入) + sms: + # 访问密钥ID(可复用OCR的AK,需开通短信服务权限) + access-key-id: + # 访问密钥Secret + access-key-secret: + # 短信签名名称(如:商贸平台) + sign-name: + # 短信模板Code(如:SMS_123456789),模板内容示例:您的验证码为${code},有效期3分钟 + template-code: + +#请求验签配置(前后端通过AES加解密共享密钥生成签名,防篡改+防重放,一次请求只能用一次) +sign: + # 是否开启验签 + enabled: true + # 前后端共享密钥(需与前端保持一致) + secret: 5e0dd8b5bb3a37519d3e3b46c2c6b200 + # 时间戳有效窗口(秒),超过该时间视为过期请求 + timeout: 300 + # 防重放nonce在Redis中的存活时间(秒) + nonce-expire: 300 + # 忽略验签的URL列表(支持Ant通配符) + ignore-urls: + - /auth/login + - /auth/code + - /auth/logout + - /auth/register + - /auth/sms/send + - /error + - /swagger-ui.html + - /swagger-resources/** + - /webjars/** + - /druid/** + - /avatar/** + - /file/** + - /api/crawler/** + - /api/test/** diff --git a/src/main/sql/商贸公司/hxrd/application-prod.yml b/src/main/sql/商贸公司/hxrd/application-prod.yml new file mode 100644 index 0000000..26956f6 --- /dev/null +++ b/src/main/sql/商贸公司/hxrd/application-prod.yml @@ -0,0 +1,197 @@ +#配置数据源 +server: + servlet: + # 设置Servlet上下文路径,使SignFilter能正确处理忽略路径 + context-path: /trade-app + +spring: + redis: + #数据库索引 + database: ${REDIS_DB:1} + host: ${HOST_HXRD} + port: ${REDIS_PORT:16379} + password: ${REDIS_PD_HXRD} + #连接超时时间 + timeout: 5000 + datasource: + druid: + db-type: com.alibaba.druid.pool.DruidDataSource + driverClassName: net.sf.log4jdbc.sql.jdbcapi.DriverSpy + url: jdbc:log4jdbc:mysql://${HOST_HXRD}:${DB_PORT:13307}/${DB_NAME:business_shop}?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false + username: ${DB_USER:root} + password: ${DB_PD_HXRD} + # 初始连接数 + initial-size: 5 + # 最小连接数 + min-idle: 15 + # 最大连接数 + max-active: 30 + # 获取连接超时时间 + max-wait: 5000 + # 连接有效性检测时间 + time-between-eviction-runs-millis: 60000 + # 连接在池中最小生存的时间 + min-evictable-idle-time-millis: 300000 + # 连接在池中最大生存的时间 + max-evictable-idle-time-millis: 900000 + # 指明连接是否被空闲连接回收器(如果有)进行检验.如果检测失败,则连接将被从池中去除 + test-while-idle: true + # 指明是否在从池中取出连接前进行检验,如果检验失败, 则从池中去除连接并尝试取出另一个 + test-on-borrow: true + # 是否在归还到池中前进行检验 + test-on-return: false + # 检测连接是否有效 + validation-query: select 1 + # 配置监控统计 + webStatFilter: + enabled: true + stat-view-servlet: + allow: + enabled: true + # 控制台管理用户名和密码 + url-pattern: /druid/* + reset-enable: false + login-username: admin + login-password: 123456 + filter: + stat: + enabled: true + # 记录慢SQL + log-slow-sql: true + slow-sql-millis: 1000 + merge-sql: true + wall: + config: + multi-statement-allow: true + +# 登录相关配置 +login: + # 默认密码 + default-password: 123456 + # 是否限制单用户登录 + single-login: false + # Redis用户登录缓存配置 + user-cache: + # 存活时间/秒 + idle-time: 21600 + # 验证码 + login-code: + # 验证码类型配置 查看 LoginProperties 类 + code-type: arithmetic + # 登录图形验证码有效时间/分钟 + expiration: 2 + # 验证码高度 + width: 111 + # 验证码宽度 + height: 36 + # 内容长度 + length: 2 + # 字体名称,为空则使用默认字体,如遇到线上乱码,设置其他字体即可 + font-name: + # 字体大小 + font-size: 25 + +#jwt +jwt: + header: Authorization + # 令牌前缀 + token-start-with: Bearer + # 必须使用最少88位的Base64对该令牌进行编码 + base64-secret: ZmQ0ZGI5NjQ0MDQwY2I4MjMxY2Y3ZmI3MjdhN2ZmMjNhODViOTg1ZGE0NTBjMGM4NDA5NzYxMjdjOWMwYWRmZTBlZjlhNGY3ZTg4Y2U3YTE1ODVkZDU5Y2Y3OGYwZWE1NzUzNWQ2YjFjZDc0NGMxZWU2MmQ3MjY1NzJmNTE0MzI= + # 令牌过期时间 此处单位/毫秒 ,默认2小时,可在此网站生成 https://www.convertworld.com/zh-hans/time/milliseconds.html + token-validity-in-seconds: 7200000 + # 在线用户key + online-key: "online-token:" + # 验证码 + code-key: "captcha-code:" + # token 续期检查时间范围(默认30分钟,单位默认毫秒),在token即将过期的一段时间内用户操作了,则给用户的token续期 + detect: 1800000 + # 续期时间范围,默认 1小时,这里单位毫秒 + renew: 3600000 + +#是否允许生成代码,生产环境设置为false +generator: + enabled: false + +#如果生产环境要开启swagger,需要配置请求地址 +#springfox: +# documentation: +# swagger: +# v2: +# host: # 接口域名或外网ip + +#是否开启 swagger-ui +swagger: + enabled: false + +#应用上下文路径(生产环境部署时使用) +app: + context-path: /trade-app + +# 文件存储路径 +file: + mac: + path: ~/file/ + avatar: ~/avatar/ + contract: ~/contract/ + contractSign: ~/contract_sign/ + backup: ~/backup/ + linux: + path: /home/trade/mall/file/ + avatar: /home/trade/mall/avatar/ + contract: /home/trade/mall/contract/ + contractSign: /home/trade/mall/contract_sign/ + backup: /home/trade/mall/backup/ + windows: + path: D:\eladmin\file\ + avatar: D:\eladmin\avatar\ + contract: D:\eladmin\contract\ + contractSign: D:\eladmin\contract_sign\ + backup: D:\eladmin\backup\ + # 文件大小 /M + maxSize: 10 + avatarMaxSize: 5 + +# 识别图片 +aliyun: + ocr: + access-key-id: LTAI5t6soA4eB9d1vNWscLQT + access-key-secret: qABYANrAPgO9o52CooJ9ZB9CAW84Ka + endpoint: ocr-api.cn-shanghai.aliyuncs.com + # 短信服务(阿里云短信,需在阿里云控制台申请签名和模板后填入) + sms: + # 访问密钥ID(可复用OCR的AK,需开通短信服务权限) + access-key-id: + # 访问密钥Secret + access-key-secret: + # 短信签名名称(如:商贸平台) + sign-name: + # 短信模板Code(如:SMS_123456789),模板内容示例:您的验证码为${code},有效期3分钟 + template-code: + +#请求验签配置(前后端通过AES加解密共享密钥生成签名,防篡改+防重放,一次请求只能用一次) +sign: + # 是否开启验签 + enabled: true + # 前后端共享密钥(需与前端保持一致) + secret: 5e0dd8b5bb3a37519d3e3b46c2c6b200 + # 时间戳有效窗口(秒),超过该时间视为过期请求 + timeout: 300 + # 防重放nonce在Redis中的存活时间(秒) + nonce-expire: 300 + # 忽略验签的URL列表(支持Ant通配符) + ignore-urls: + - /auth/login + - /auth/code + - /auth/logout + - /auth/register + - /auth/sms/send + - /error + - /swagger-ui.html + - /swagger-resources/** + - /webjars/** + - /druid/** + - /avatar/** + - /file/** + - /api/crawler/** + - /api/test/** diff --git a/src/main/sql/商贸公司/hxrd/application.yml b/src/main/sql/商贸公司/hxrd/application.yml new file mode 100644 index 0000000..15271af --- /dev/null +++ b/src/main/sql/商贸公司/hxrd/application.yml @@ -0,0 +1,49 @@ +server: + port: 8000 + compression: + enabled: true + mime-types: text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json + +mybatis-plus: + configuration: + # 关闭二级缓存 + cache-enabled: false + # 设置本地缓存作用域 + local-cache-scope: SESSION + +spring: + freemarker: + check-template-location: false + profiles: + active: dev +# active: prod + data: + redis: + repositories: + enabled: false +# pid: +# file: /自行指定位置/eladmin.pid + +task: + pool: + # 核心线程池大小 + core-pool-size: 10 + # 最大线程数 + max-pool-size: 30 + # 活跃时间 + keep-alive-seconds: 60 + # 队列容量 + queue-capacity: 50 + +#七牛云 +qiniu: + # 文件大小 /M + max-size: 15 + +#邮箱验证码有效时间/秒 +code: + expiration: 300 + +#密码加密传输,前端公钥加密,后端私钥解密 +rsa: + private_key: MIIBUwIBADANBgkqhkiG9w0BAQEFAASCAT0wggE5AgEAAkEA0vfvyTdGJkdbHkB8mp0f3FE0GYP3AYPaJF7jUd1M0XxFSE2ceK3k2kw20YvQ09NJKk+OMjWQl9WitG9pB6tSCQIDAQABAkA2SimBrWC2/wvauBuYqjCFwLvYiRYqZKThUS3MZlebXJiLB+Ue/gUifAAKIg1avttUZsHBHrop4qfJCwAI0+YRAiEA+W3NK/RaXtnRqmoUUkb59zsZUBLpvZgQPfj1MhyHDz0CIQDYhsAhPJ3mgS64NbUZmGWuuNKp5coY2GIj/zYDMJp6vQIgUueLFXv/eZ1ekgz2Oi67MNCk5jeTF2BurZqNLR3MSmUCIFT3Q6uHMtsB9Eha4u7hS31tj1UWE+D+ADzp59MGnoftAiBeHT7gDMuqeJHPL4b+kC+gzV4FGTfhR9q3tTbklZkD2A== diff --git a/src/main/sql/商贸公司/hxrd/pom.xml b/src/main/sql/商贸公司/hxrd/pom.xml new file mode 100644 index 0000000..45c25bd --- /dev/null +++ b/src/main/sql/商贸公司/hxrd/pom.xml @@ -0,0 +1,145 @@ + + + + eladmin + me.zhengjie + 1.1 + + 4.0.0 + + eladmin-system + 核心模块 + + + 0.11.5 + + 5.8.0 + + + + + + me.zhengjie + eladmin-generator + 1.1 + + + me.zhengjie + eladmin-common + + + + + + + me.zhengjie + eladmin-tools + 1.1 + + + + + org.springframework.boot + spring-boot-starter-websocket + + + + + io.jsonwebtoken + jjwt-api + ${jjwt.version} + + + io.jsonwebtoken + jjwt-impl + ${jjwt.version} + + + io.jsonwebtoken + jjwt-jackson + ${jjwt.version} + + + + + org.quartz-scheduler + quartz + + + + + ch.ethz.ganymed + ganymed-ssh2 + build210 + + + com.jcraft + jsch + 0.1.55 + + + + + com.github.oshi + oshi-core + 6.1.4 + + + + + com.aliyun + ocr_api20210707 + 3.1.3 + + + + + com.aliyun + dysmsapi20170525 + 2.0.24 + + + + + + + trade-app-${pkg.timestamp} + + + + org.codehaus.mojo + buildnumber-maven-plugin + 1.4 + + + generate-build-timestamp + initialize + + create-timestamp + + + + + yyyyMMddHHmmss + pkg.timestamp + Asia/Shanghai + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + + diff --git a/src/main/sql/数据切割/过程和步骤.sql b/src/main/sql/数据切割/过程和步骤.sql index 49f68d5..fa2fd2c 100644 --- a/src/main/sql/数据切割/过程和步骤.sql +++ b/src/main/sql/数据切割/过程和步骤.sql @@ -1,4 +1,6 @@ - +1,先用代码同步 用户表、货物表等 +2,在把表同步到商贸公司的数据库里 +3,执行插入脚本: INSERT INTO member_info ( id, diff --git a/src/test/java/com/qdw/buyerTest.java b/src/test/java/com/qdw/buyerTest.java new file mode 100644 index 0000000..442c922 --- /dev/null +++ b/src/test/java/com/qdw/buyerTest.java @@ -0,0 +1,898 @@ +package com.qdw; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * 企得物(QDW)卖家后台 - 登录 + 货物列表获取 + * + * 流程: + * 1. POST /api/user/login 登录获取Token + * 2. GET /api/home/on-sell-orders 获取在售货物列表 + */ +public class buyerTest { + + private static final String LOGIN_URL = "https://web.qdwsmgs.com/api/user/login"; + private static final String GOODS_LIST_URL = "https://web.qdwsmgs.com/api/home/on-sell-orders"; + private static final String CLAIM_ORDER_URL = "https://web.qdwsmgs.com/api/order/%d/claim"; + private static final String PHONE = "15313852029"; + private static final String PASSWORD = "123456"; + private static final int MAX_PRICE = 38000; + private static final int MAX_BUY_NUM = 2; + + private static final int PAGE_SIZE = 20; + + /** + * 主流程:登录 → 分页获取所有货物 → 按价格排序 → 输出 + */ + public static void main(String[] args) { + try { + System.out.println("============================================"); + System.out.println(" 企得物卖家后台 - 登录 & 全量货物列表(按价格排序)"); + System.out.println("============================================"); + + // 步骤1: 登录 + System.out.println("\n[1] 正在登录..."); + LoginResult loginResult = doLogin(PHONE, PASSWORD, true); + + if (!loginResult.isSuccess()) { + System.out.println("登录失败: " + loginResult.getMessage()); + return; + } + + System.out.println("登录成功! 用户: " + loginResult.getNickname() + + " (ID: " + loginResult.getUserId() + ")"); + + String token = loginResult.getToken(); + + // 步骤2: 分页获取所有货物 + System.out.println("\n[2] 正在分页获取全部货物..."); + List allGoods = fetchAllGoods(token); + + if (allGoods.isEmpty()) { + System.out.println("未获取到任何货物数据"); + return; + } + + System.out.println("\n共获取 " + allGoods.size() + " 条货物记录"); + + // 步骤3: 按价格倒序排序 + System.out.println("\n[3] 按价格倒序排序..."); + Collections.sort(allGoods, Comparator.comparingDouble(GoodsItem::getPrice).reversed()); + + // 步骤4: 输出排序后的列表 + System.out.println("\n[4] 按价格排序后的货物列表:"); + System.out.println("──────────────────────────────────────────────────────────────────────────────"); + System.out.printf("%-6s %-8s %-10s %-12s %-30s %-12s%n", + "序号", "ID", "价格", "商品原价", "商品标题", "创建时间"); + System.out.println("──────────────────────────────────────────────────────────────────────────────"); + + int index = 1; + for (GoodsItem g : allGoods) { + System.out.printf("%-6d %-8d %-10.2f %-12s %-30s %-12s%n", + index++, + g.getId(), + g.getPrice(), + g.getProductPriceStr(), + truncate(g.getProductTitle(), 28), + g.getCreatedAt()); + } + + System.out.println("──────────────────────────────────────────────────────────────────────────────"); + System.out.println("总计: " + allGoods.size() + " 条记录"); + + // 统计信息 + double minPrice = allGoods.get(0).getPrice(); + double maxPrice = allGoods.get(allGoods.size() - 1).getPrice(); + double sum = 0; + for (GoodsItem g : allGoods) sum += g.getPrice(); + double avgPrice = sum / allGoods.size(); + + System.out.println("\n价格统计:"); + System.out.printf(" 最低价: %.2f%n", minPrice); + System.out.printf(" 最高价: %.2f%n", maxPrice); + System.out.printf(" 平均价: %.2f%n", avgPrice); + System.out.printf(" 总价: %.2f%n", sum); + + // 步骤5: 筛选价格 < MAX_PRICE 的货物,按价格升序,依次下单(最多 MAX_BUY_NUM 单) + System.out.println("\n[5] 筛选价格 < " + MAX_PRICE + " 的货物并尝试下单(最多" + MAX_BUY_NUM + "单)..."); + List buyCandidates = new ArrayList<>(); + for (GoodsItem g : allGoods) { + if (g.getPrice() < MAX_PRICE) { + buyCandidates.add(g); + } + } + // 按价格从大到小排序,高价优先 + buyCandidates.sort(Comparator.comparingDouble(GoodsItem::getPrice).reversed()); + System.out.println(" 符合条件货物数: " + buyCandidates.size()); + + int boughtCount = 0; + for (GoodsItem target : buyCandidates) { + if (boughtCount >= MAX_BUY_NUM) { + System.out.println("\n 已达最大购买数量 " + MAX_BUY_NUM + ",停止下单"); + break; + } + System.out.println("\n 第 " + (boughtCount + 1) + " 单 → ID=" + target.getId() + + " 价格=" + String.format("%.2f", target.getPrice()) + + " 标题=" + truncate(target.getProductTitle(), 24)); + + ClaimResult claim = claimOrder(token, target.getId(), true); + System.out.println(" 下单结果: " + claim); + + if (claim.isSuccess()) { + boughtCount++; + System.out.println(" 下单成功! 已购买 " + boughtCount + "/" + MAX_BUY_NUM); + } else if ("3004".equals(claim.getCode())) { + System.out.println("\n 业务侧返回 code=3004 (活动已结束),结束程序"); + return; + } else { + System.out.println(" 下单失败,尝试下一个"); + } + } + System.out.println("\n[完成] 共成功下单 " + boughtCount + " 单"); + + } catch (Exception e) { + System.err.println("执行异常: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * 分页获取所有货物,返回全部记录 + */ + private static List fetchAllGoods(String token) throws Exception { + List allGoods = new ArrayList<>(); + int page = 1; + int total = 0; + + while (true) { + System.out.printf(" 正在获取第 %d 页 (pageSize=%d)...", page, PAGE_SIZE); + String json = fetchGoodsList(token, page, PAGE_SIZE, true); + + if (json == null || json.isEmpty()) { + System.out.println(" 失败(无响应)"); + break; + } + + // 解析本页数据 + String dataJson = extractObject(json, "\"data\":"); + if (dataJson == null) { + System.out.println(" 失败(无data)"); + break; + } + + // 第一页时读取 total + if (page == 1) { + String totalStr = extractValue(dataJson, "\"total\":", ","); + if (totalStr != null) { + try { total = Integer.parseInt(totalStr); } catch (NumberFormatException ignored) {} + } + System.out.println(" 总记录数=" + total); + } else { + System.out.println(); + } + + // 解析 records 数组 + String listJson = extractArray(dataJson, "\"records\":"); + if (listJson == null) { + listJson = extractArray(dataJson, "\"list\":"); + } + if (listJson == null) { + System.out.println(" 第" + page + "页未找到记录数组"); + break; + } + + List items = splitArrayElements(listJson); + for (String item : items) { + GoodsItem goods = parseGoodsItem(item); + if (goods != null) { + allGoods.add(goods); + } + } + + // 判断是否还有下一页 + if (items.isEmpty() || allGoods.size() >= total) { + break; + } + page++; + } + + return allGoods; + } + + /** + * 解析单条货物为 GoodsItem 对象 + */ + private static GoodsItem parseGoodsItem(String json) { + GoodsItem g = new GoodsItem(); + List fields = parseAllFields(json); + for (String[] f : fields) { + String key = f[0]; + String val = f[1]; + switch (key) { + case "id": + try { g.setId(Integer.parseInt(val)); } catch (NumberFormatException ignored) {} + break; + case "sellerId": + try { g.setSellerId(Integer.parseInt(val)); } catch (NumberFormatException ignored) {} + break; + case "productId": + try { g.setProductId(Integer.parseInt(val)); } catch (NumberFormatException ignored) {} + break; + case "productTitle": g.setProductTitle(val); break; + case "productThumbnail": g.setProductThumbnail(val); break; + case "price": g.setPrice(parseDouble(val)); break; + case "productPrice": g.setProductPriceStr(val); break; + case "isDisplay": + try { g.setDisplay(Integer.parseInt(val)); } catch (NumberFormatException ignored) {} + break; + case "status": + try { g.setStatus(Integer.parseInt(val)); } catch (NumberFormatException ignored) {} + break; + case "createdAt": g.setCreatedAt(val); break; + case "updatedAt": g.setUpdatedAt(val); break; + } + } + return g; + } + + private static String truncate(String s, int max) { + if (s == null) return ""; + return s.length() > max ? s.substring(0, max) + "…" : s; + } + + // ==================== 登录相关 ==================== + + /** + * 执行登录请求 + */ + public static LoginResult doLogin(String phone, String password, boolean rememberMe) throws Exception { + HttpClient client = buildHttpClient(); + + String body = String.format("{\"phone\":\"%s\",\"password\":\"%s\",\"rememberMe\":%b}", + phone, password, rememberMe); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(LOGIN_URL)) + .timeout(java.time.Duration.ofSeconds(60)) + .header("Content-Type", "application/json") + .header("Accept", "application/json, text/plain, */*") + .header("Origin", "https://web.qdwsmgs.com") + .header("Referer", "https://web.qdwsmgs.com/login") + .header("User-Agent", buildUserAgent()) + .POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + return parseLoginResponse(response.body()); + } + + // ==================== 货物列表相关 ==================== + + /** + * 获取在售货物列表 + */ + private static String fetchGoodsList(String token, int page, int pageSize, boolean withSign) throws Exception { + HttpClient client = buildHttpClient(); + + String url = GOODS_LIST_URL + "?page=" + page + "&pageSize=" + pageSize; + + HttpRequest.Builder reqBuilder = HttpRequest.newBuilder() + .uri(URI.create(url)) + .timeout(java.time.Duration.ofSeconds(60)) + .header("Accept", "application/json, text/plain, */*") + .header("Origin", "https://web.qdwsmgs.com") + .header("Referer", "https://web.qdwsmgs.com/exchange/list") + .header("User-Agent", buildUserAgent()) + .header("Authorization", "Bearer " + token); + + if (withSign) { + String timestamp = String.valueOf(System.currentTimeMillis()); + String nonce = generateNonce(); + String sign = md5(nonce + timestamp); + reqBuilder.header("X-Nonce", nonce) + .header("X-Sign", sign) + .header("X-Timestamp", timestamp); + } + + HttpRequest request = reqBuilder.GET().build(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() == 200) { + return response.body(); + } + return null; + } + + /** + * 下单(认领订单):POST /api/order/{orderId}/claim + * 请求体为空(Content-Length: 0),Authorization + 签名头 + */ + private static ClaimResult claimOrder(String token, int orderId, boolean withSign) throws Exception { + HttpClient client = buildHttpClient(); + + String url = String.format(CLAIM_ORDER_URL, orderId); + HttpRequest.Builder reqBuilder = HttpRequest.newBuilder() + .uri(URI.create(url)) + .timeout(java.time.Duration.ofSeconds(60)) + .header("Accept", "application/json, text/plain, */*") + .header("Origin", "https://web.qdwsmgs.com") + .header("Referer", "https://web.qdwsmgs.com/exchange/list") + .header("User-Agent", buildUserAgent()) + .header("Authorization", "Bearer " + token) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.noBody()); + + if (withSign) { + String timestamp = String.valueOf(System.currentTimeMillis()); + String nonce = generateNonce(); + String sign = md5(nonce + timestamp); + reqBuilder.header("X-Nonce", nonce) + .header("X-Sign", sign) + .header("X-Timestamp", timestamp); + } + + HttpRequest request = reqBuilder.build(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + ClaimResult result = new ClaimResult(); + result.setHttpStatus(response.statusCode()); + String body = response.body(); + result.setRawJson(body == null ? "" : body); + + if (body != null && !body.isEmpty()) { + String code = extractValue(body, "\"code\":", ","); + String msg = extractStringValue(body, "\"message\":"); + String dataPart = extractObject(body, "\"data\":"); + result.setCode(code == null ? "" : code); + result.setMessage(msg == null ? "" : msg); + result.setSuccess("200".equals(code) || "0".equals(code) + || (code == null && response.statusCode() == 200)); + result.setData(dataPart == null ? "" : dataPart); + } else { + result.setSuccess(response.statusCode() == 200); + } + + return result; + } + + /** + * 解析并打印货物列表 + */ + private static void parseAndPrintGoodsList(String json) { + System.out.println("\n========== 接口原始响应(前500字符) =========="); + String preview = json.length() > 500 ? json.substring(0, 500) + "..." : json; + System.out.println(preview); + System.out.println("=============================================\n"); + + // 解析 code + String code = extractValue(json, "\"code\":", ","); + String message = extractStringValue(json, "\"message\":"); + System.out.println("接口状态码: " + code); + System.out.println("接口消息: " + message); + + if (!"200".equals(code) && !"0".equals(code)) { + System.out.println("接口返回错误,无法解析货物列表"); + return; + } + + // 解析 data 部分 + String dataJson = extractObject(json, "\"data\":"); + if (dataJson == null) { + // 可能直接返回列表 + if (json.startsWith("[")) { + dataJson = json; + } else { + System.out.println("未找到 data 字段"); + return; + } + } + + // 解析分页信息 + String total = extractValue(dataJson, "\"total\":", ","); + String pageSize = extractValue(dataJson, "\"pageSize\":", ","); + String page = extractValue(dataJson, "\"page\":", ","); + System.out.println("\n分页信息:"); + System.out.println(" 总数量: " + total); + System.out.println(" 当前页: " + page); + System.out.println(" 每页大小: " + pageSize); + + // 解析货物列表 + String listJson = extractArray(dataJson, "\"list\":"); + if (listJson == null) { + listJson = extractArray(dataJson, "\"records\":"); + } + if (listJson == null) { + listJson = extractArray(dataJson, "\"items\":"); + } + + if (listJson == null) { + System.out.println("\n未找到列表字段(list/records/items)"); + System.out.println("data 部分内容预览: " + (dataJson.length() > 300 + ? dataJson.substring(0, 300) + "..." : dataJson)); + return; + } + + List goodsItems = splitArrayElements(listJson); + System.out.println("\n========== 在售货物列表 (共" + goodsItems.size() + "条) =========="); + + int index = 1; + for (String item : goodsItems) { + System.out.println("\n--- 货物 #" + index++ + " ---"); + printGoodsItem(item); + } + + System.out.println("\n============================================="); + } + + /** + * 打印单条货物信息 - 解析所有字段 + */ + private static void printGoodsItem(String item) { + // 遍历 JSON 对象中所有 key-value 对 + List fields = parseAllFields(item); + for (String[] field : fields) { + String key = field[0]; + String val = field[1]; + // 跳过 null 值 + if ("null".equals(val) || val.isEmpty()) continue; + System.out.println(" " + key + ": " + val); + } + } + + /** + * 解析 JSON 对象中所有顶层 key-value 对 + * 返回 [[key, value], [key, value], ...] + */ + private static List parseAllFields(String json) { + List fields = new ArrayList<>(); + if (json == null || !json.startsWith("{")) return fields; + + int pos = 1; // skip '{' + int len = json.length() - 1; // skip '}' + + while (pos < len) { + // 跳过空白和逗号 + while (pos < len && (json.charAt(pos) == ',' || json.charAt(pos) == ' ' || json.charAt(pos) == '\n')) { + pos++; + } + if (pos >= len) break; + + // 读取 key (必须是字符串) + if (json.charAt(pos) != '"') { pos++; continue; } + String key = readJsonString(json, pos); + pos += key.length() + 2; // skip closing quote + + // 跳过冒号 + while (pos < len && json.charAt(pos) != ':') pos++; + pos++; // skip ':' + + // 跳过空白 + while (pos < len && (json.charAt(pos) == ' ' || json.charAt(pos) == '\n')) pos++; + + // 读取 value + String value; + char c = json.charAt(pos); + if (c == '"') { + // 字符串值 + value = readJsonString(json, pos); + pos += value.length() + 2; + } else if (c == '{' || c == '[') { + // 嵌套对象或数组 - 跳到匹配位置 + int end = findMatchingBracket(json, pos); + value = json.substring(pos, end + 1); + pos = end + 1; + } else { + // 数字、布尔、null + int start = pos; + while (pos < len && json.charAt(pos) != ',' && json.charAt(pos) != '}') { + pos++; + } + value = json.substring(start, pos).trim(); + } + + fields.add(new String[]{key, value}); + } + + return fields; + } + + /** + * 读取 JSON 字符串值(处理转义) + */ + private static String readJsonString(String json, int pos) { + // pos 指向开头的 " + int i = pos + 1; + StringBuilder sb = new StringBuilder(); + while (i < json.length()) { + char c = json.charAt(i); + if (c == '\\' && i + 1 < json.length()) { + char next = json.charAt(i + 1); + switch (next) { + case '"': sb.append('"'); i += 2; break; + case '\\': sb.append('\\'); i += 2; break; + case '/': sb.append('/'); i += 2; break; + case 'n': sb.append('\n'); i += 2; break; + case 't': sb.append('\t'); i += 2; break; + case 'r': sb.append('\r'); i += 2; break; + default: sb.append(c); i++; break; + } + } else if (c == '"') { + return sb.toString(); + } else { + sb.append(c); + i++; + } + } + return sb.toString(); + } + + /** + * 找到匹配的括号位置 + */ + private static int findMatchingBracket(String json, int pos) { + char open = json.charAt(pos); + char close = open == '{' ? '}' : ']'; + int depth = 0; + boolean inString = false; + for (int i = pos; i < json.length(); i++) { + char c = json.charAt(i); + if (c == '\\' && inString) { i++; continue; } + if (c == '"') inString = !inString; + if (inString) continue; + if (c == open) depth++; + else if (c == close) { + depth--; + if (depth == 0) return i; + } + } + return json.length() - 1; + } + + // ==================== HTTP 工具方法 ==================== + + private static HttpClient buildHttpClient() { + return HttpClient.newBuilder() + .connectTimeout(java.time.Duration.ofSeconds(30)) + .followRedirects(HttpClient.Redirect.NORMAL) + .build(); + } + + private static String buildUserAgent() { + return "Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) " + + "AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1"; + } + + // ==================== 登录响应解析 ==================== + + private static LoginResult parseLoginResponse(String json) { + LoginResult result = new LoginResult(); + try { + result.setRawJson(json); + String code = extractValue(json, "\"code\":", ","); + result.setSuccess("200".equals(code) || "0".equals(code)); + result.setMessage(extractStringValue(json, "\"message\":")); + + String tokenJson = extractObject(json, "\"data\":"); + if (tokenJson != null) { + result.setToken(extractStringValue(tokenJson, "\"token\":")); + } + + String userJson = extractObject(json, "\"user\":"); + if (userJson != null) { + result.setUserId(extractValue(userJson, "\"id\":", ",")); + result.setPhone(extractStringValue(userJson, "\"phone\":")); + result.setNickname(extractStringValue(userJson, "\"nickname\":")); + result.setLevelId(extractValue(userJson, "\"levelId\":", ",")); + result.setLevelName(extractStringValue(userJson, "\"levelName\":")); + result.setPromotionCode(extractStringValue(userJson, "\"promotionCode\":")); + result.setProfitBalance(parseDouble(extractValue(userJson, "\"profitBalance\":", ","))); + result.setCouponBalance(parseDouble(extractValue(userJson, "\"couponBalance\":", ","))); + } + result.setTimestamp(extractValue(json, "\"timestamp\":", "}")); + } catch (Exception e) { + result.setMessage("响应解析失败: " + e.getMessage()); + } + return result; + } + + // ==================== 签名工具 ==================== + + private static String generateNonce() { + String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 28; i++) { + sb.append(chars.charAt((int) (Math.random() * chars.length()))); + } + return sb.toString(); + } + + private static String md5(String input) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] digest = md.digest(input.getBytes(StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(); + for (byte b : digest) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } catch (Exception e) { + throw new RuntimeException("MD5计算失败", e); + } + } + + // ==================== JSON 解析辅助方法 ==================== + + private static String extractStringValue(String json, String key) { + int keyIdx = json.indexOf(key); + if (keyIdx < 0) return null; + int start = keyIdx + key.length(); + if (start < json.length() && json.charAt(start) == '"') { + start++; + int end = json.indexOf('"', start); + if (end > start) { + return json.substring(start, end); + } + } + return null; + } + + private static String extractValue(String json, String key, String delimiter) { + int keyIdx = json.indexOf(key); + if (keyIdx < 0) return null; + int start = keyIdx + key.length(); + int end = json.indexOf(delimiter, start); + if (end > start) { + return json.substring(start, end).trim(); + } + String sub = json.substring(start).trim(); + if (sub.endsWith("}")) sub = sub.substring(0, sub.length() - 1); + return sub.trim(); + } + + private static String extractObject(String json, String key) { + int keyIdx = json.indexOf(key); + if (keyIdx < 0) return null; + int start = json.indexOf('{', keyIdx); + if (start < 0) return null; + int depth = 0; + for (int i = start; i < json.length(); i++) { + char c = json.charAt(i); + if (c == '{') depth++; + else if (c == '}') { + depth--; + if (depth == 0) { + return json.substring(start, i + 1); + } + } + } + return null; + } + + /** + * 提取 JSON 数组 + */ + private static String extractArray(String json, String key) { + int keyIdx = json.indexOf(key); + if (keyIdx < 0) return null; + int start = json.indexOf('[', keyIdx); + if (start < 0) return null; + int depth = 0; + for (int i = start; i < json.length(); i++) { + char c = json.charAt(i); + if (c == '[') depth++; + else if (c == ']') { + depth--; + if (depth == 0) { + return json.substring(start, i + 1); + } + } + } + return null; + } + + /** + * 将 JSON 数组拆分为单个元素 + */ + private static List splitArrayElements(String arrayJson) { + List elements = new ArrayList<>(); + if (arrayJson == null || arrayJson.length() < 2) return elements; + + // 去掉首尾的 [ 和 ] + String inner = arrayJson.substring(1, arrayJson.length() - 1).trim(); + if (inner.isEmpty()) return elements; + + // 按顶级花括号拆分 + int depth = 0; + int start = -1; + boolean inString = false; + boolean escape = false; + + for (int i = 0; i < inner.length(); i++) { + char c = inner.charAt(i); + + if (escape) { + escape = false; + continue; + } + if (c == '\\') { + escape = true; + continue; + } + if (c == '"') { + inString = !inString; + continue; + } + if (inString) continue; + + if (c == '{') { + if (depth == 0) start = i; + depth++; + } else if (c == '}') { + depth--; + if (depth == 0 && start >= 0) { + elements.add(inner.substring(start, i + 1)); + start = -1; + } + } + } + + // 如果没有找到对象,尝试按逗号拆分基本类型 + if (elements.isEmpty() && inner.startsWith("\"")) { + // 字符串数组 + int s = 0; + for (int i = 0; i < inner.length(); i++) { + if (inner.charAt(i) == '"' && (i == 0 || inner.charAt(i - 1) != '\\')) { + if (s == 0) { s = i + 1; } + else { + elements.add("\"" + inner.substring(s, i) + "\""); + s = 0; + } + } + } + } + + return elements; + } + + private static double parseDouble(String val) { + if (val == null || val.isEmpty()) return 0.0; + try { + return Double.parseDouble(val); + } catch (NumberFormatException e) { + return 0.0; + } + } + + // ==================== 数据类 ==================== + + public static class LoginResult { + private boolean success; + private String message; + private String token; + private String userId; + private String phone; + private String nickname; + private String levelId; + private String levelName; + private String promotionCode; + private double profitBalance; + private double couponBalance; + private String timestamp; + private String rawJson; + + public boolean isSuccess() { return success; } + public void setSuccess(boolean success) { this.success = success; } + public String getMessage() { return message; } + public void setMessage(String message) { this.message = message; } + public String getToken() { return token; } + public void setToken(String token) { this.token = token; } + public String getUserId() { return userId; } + public void setUserId(String userId) { this.userId = userId; } + public String getPhone() { return phone; } + public void setPhone(String phone) { this.phone = phone; } + public String getNickname() { return nickname; } + public void setNickname(String nickname) { this.nickname = nickname; } + public String getLevelId() { return levelId; } + public void setLevelId(String levelId) { this.levelId = levelId; } + public String getLevelName() { return levelName; } + public void setLevelName(String levelName) { this.levelName = levelName; } + public String getPromotionCode() { return promotionCode; } + public void setPromotionCode(String promotionCode) { this.promotionCode = promotionCode; } + public double getProfitBalance() { return profitBalance; } + public void setProfitBalance(double profitBalance) { this.profitBalance = profitBalance; } + public double getCouponBalance() { return couponBalance; } + public void setCouponBalance(double couponBalance) { this.couponBalance = couponBalance; } + public String getTimestamp() { return timestamp; } + public void setTimestamp(String timestamp) { this.timestamp = timestamp; } + public String getRawJson() { return rawJson; } + public void setRawJson(String rawJson) { this.rawJson = rawJson; } + } + + /** + * 货物项数据类 + */ + public static class GoodsItem { + private int id; + private int sellerId; + private int productId; + private String productTitle; + private String productThumbnail; + private double price; + private String productPriceStr; + private int display; + private int status; + private String createdAt; + private String updatedAt; + + public int getId() { return id; } + public void setId(int id) { this.id = id; } + public int getSellerId() { return sellerId; } + public void setSellerId(int sellerId) { this.sellerId = sellerId; } + public int getProductId() { return productId; } + public void setProductId(int productId) { this.productId = productId; } + public String getProductTitle() { return productTitle; } + public void setProductTitle(String productTitle) { this.productTitle = productTitle; } + public String getProductThumbnail() { return productThumbnail; } + public void setProductThumbnail(String productThumbnail) { this.productThumbnail = productThumbnail; } + public double getPrice() { return price; } + public void setPrice(double price) { this.price = price; } + public String getProductPriceStr() { return productPriceStr; } + public void setProductPriceStr(String productPriceStr) { this.productPriceStr = productPriceStr; } + public int getDisplay() { return display; } + public void setDisplay(int display) { this.display = display; } + public int getStatus() { return status; } + public void setStatus(int status) { this.status = status; } + public String getCreatedAt() { return createdAt; } + public void setCreatedAt(String createdAt) { this.createdAt = createdAt; } + public String getUpdatedAt() { return updatedAt; } + public void setUpdatedAt(String updatedAt) { this.updatedAt = updatedAt; } + } + + /** + * 下单(claim)结果 + */ + public static class ClaimResult { + private int httpStatus; + private String code; + private String message; + private boolean success; + private String data; + private String rawJson; + + public int getHttpStatus() { return httpStatus; } + public void setHttpStatus(int httpStatus) { this.httpStatus = httpStatus; } + public String getCode() { return code; } + public void setCode(String code) { this.code = code; } + public String getMessage() { return message; } + public void setMessage(String message) { this.message = message; } + public boolean isSuccess() { return success; } + public void setSuccess(boolean success) { this.success = success; } + public String getData() { return data; } + public void setData(String data) { this.data = data; } + public String getRawJson() { return rawJson; } + public void setRawJson(String rawJson) { this.rawJson = rawJson; } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("HTTP=").append(httpStatus) + .append(", success=").append(success) + .append(", code=").append(code == null ? "" : code) + .append(", msg=").append(message == null ? "" : message); + if (data != null && !data.isEmpty()) { + String dv = data.length() > 120 ? data.substring(0, 120) + "…" : data; + sb.append(", data=").append(dv); + } + return sb.toString(); + } + } +} \ No newline at end of file