Compare commits

...

4 Commits

Author SHA1 Message Date
Leo
72ff30567b 1 2026-01-26 22:31:46 +08:00
Leo
caa36c4966 1 2026-01-19 21:22:55 +08:00
Leo
269e8e48a7 1 2026-01-19 20:53:46 +08:00
Leo
dde274acba 1 2026-01-17 23:52:56 +08:00
7 changed files with 303 additions and 50 deletions

View File

@@ -1566,8 +1566,9 @@ public class TencentDocController extends BaseController {
log.info(logMsg);
// 更新订单的推送状态(重新查询订单,避免使用旧对象)
JDOrder orderToUpdate = null;
try {
JDOrder orderToUpdate = jdOrderService.selectJDOrderByThirdPartyOrderNo(expectedOrderNo);
orderToUpdate = jdOrderService.selectJDOrderByThirdPartyOrderNo(expectedOrderNo);
if (orderToUpdate != null) {
orderToUpdate.setTencentDocPushed(1);
orderToUpdate.setTencentDocPushTime(new java.util.Date());
@@ -1597,6 +1598,10 @@ public class TencentDocController extends BaseController {
if (phone != null) {
successLog.put("phone", phone);
}
// 添加型号信息
if (orderToUpdate != null && orderToUpdate.getModelNumber() != null) {
successLog.put("modelNumber", orderToUpdate.getModelNumber());
}
// 检查是否为物流链接更新(复用之前的变量)
if (Boolean.TRUE.equals(isLinkUpdated)) {
@@ -2766,9 +2771,6 @@ public class TencentDocController extends BaseController {
content.append("【批量同步-腾讯文档同步成功】\n\n");
}
content.append(String.format("✓ 成功填充: %d 条\n", filledCount));
if (skippedCount > 0) {
content.append(String.format("⊘ 跳过: %d 条\n", skippedCount));
}
if (errorCount > 0) {
content.append(String.format("✗ 错误: %d 条\n", errorCount));
}
@@ -2783,20 +2785,43 @@ public class TencentDocController extends BaseController {
content.append("\n");
if (!successLogs.isEmpty()) {
// 统计今天的型号
Map<String, Integer> modelCountMap = new java.util.HashMap<>();
for (Map<String, Object> log : successLogs) {
String modelNumber = (String) log.get("modelNumber");
if (modelNumber != null && !modelNumber.trim().isEmpty()) {
modelCountMap.put(modelNumber, modelCountMap.getOrDefault(modelNumber, 0) + 1);
} else {
modelCountMap.put("未知型号", modelCountMap.getOrDefault("未知型号", 0) + 1);
}
}
// 显示今天的型号统计
if (!modelCountMap.isEmpty()) {
content.append("【今天的型号统计】\n");
List<Map.Entry<String, Integer>> sortedModels = new java.util.ArrayList<>(modelCountMap.entrySet());
sortedModels.sort((a, b) -> b.getValue().compareTo(a.getValue())); // 按数量降序排序
for (Map.Entry<String, Integer> entry : sortedModels) {
content.append(String.format("型号: %s 数量: %d\n", entry.getKey(), entry.getValue()));
}
content.append("\n");
}
// 显示每一单具体的型号
content.append("【成功详情】\n");
// 最多显示20条详细记录避免消息过长
int maxDisplay = Math.min(20, successLogs.size());
for (int i = 0; i < maxDisplay; i++) {
Map<String, Object> log = successLogs.get(i);
String orderNo = (String) log.get("orderNo");
Integer row = (Integer) log.get("row");
String logisticsLink = (String) log.get("logisticsLink");
String phone = (String) log.get("phone");
String modelNumber = (String) log.get("modelNumber");
content.append(String.format("%d. 单号: %s\n", i + 1, orderNo));
content.append(String.format(" 行号: %d\n", row));
if (phone != null && !phone.isEmpty()) {
content.append(String.format(" 电话: %s\n", phone));
if (modelNumber != null && !modelNumber.trim().isEmpty()) {
content.append(String.format(" 型号: %s\n", modelNumber));
} else {
content.append(" 型号: 未知\n");
}
// 检查是否为物流链接更新

View File

@@ -121,6 +121,30 @@ public class JDOrder extends BaseEntity {
@Excel(name = "后返到账日期", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date rebateReceivedDate;
/** 点过价保0否 1是 */
@Excel(name = "点过价保")
private Integer isPriceProtected;
/** 价保日期 */
@Excel(name = "价保日期", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date priceProtectedDate;
/** 开过专票0否 1是 */
@Excel(name = "开过专票")
private Integer isInvoiceOpened;
/** 开票日期 */
@Excel(name = "开票日期", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date invoiceOpenedDate;
/** 晒过评价0否 1是 */
@Excel(name = "晒过评价")
private Integer isReviewPosted;
/** 评价日期 */
@Excel(name = "评价日期", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date reviewPostedDate;
}

View File

@@ -627,11 +627,15 @@ public class InstructionServiceImpl implements IInstructionService {
}
// ==================== 按下单人统计结束 ====================
// 在返回数据前,在头部添加日期信息
// 在返回数据前,在每个元素的头部添加日期信息
if (!outputs.isEmpty()) {
LocalDate queryDate = range.get(0);
String dateStr = queryDate.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd"));
outputs.add(0, "日期:" + dateStr + "\n");
String dateHeader = "日期:" + dateStr + "\n";
// 在每个原有元素的头部追加日期信息
for (int i = 0; i < outputs.size(); i++) {
outputs.set(i, dateHeader + outputs.get(i));
}
}
return outputs.isEmpty() ? Collections.singletonList("无数据") : outputs;

View File

@@ -226,29 +226,104 @@ public class WPS365ApiServiceImpl implements IWPS365ApiService {
// 如果用户只提供了一个IDfileId则fileId和worksheetId使用同一个值
String baseUrl = "https://openapi.wps.cn/v7";
// 如果worksheetId为空或与fileId相同说明用户只配置了一个ID类似腾讯文档
// 在AirSheet中这个ID既是file_id也是worksheet_id
// 根据WPS365官方文档AirSheet的worksheet_id必须是整数
// 如果worksheetId为空或"0"使用0第一个工作表
// 如果worksheetId与fileId相同说明用户只配置了一个ID尝试使用fileId作为worksheetId
String wsId;
if (worksheetId == null || worksheetId.trim().isEmpty() || worksheetId.equals(fileId)) {
// 使用fileId作为worksheetIdAirSheet中文件ID就是工作表ID
if (worksheetId == null || worksheetId.trim().isEmpty() || "0".equals(worksheetId)) {
// 默认使用0第一个工作表)
wsId = "0";
} else if (worksheetId.equals(fileId)) {
// 如果worksheetId与fileId相同说明用户只配置了一个ID类似腾讯文档
// 在AirSheet中这个ID可能既是file_id也是worksheet_id
wsId = fileId;
} else {
// 使用提供的worksheetId可能是整数索引,如"0"
// 使用提供的worksheetId应该是整数)
wsId = worksheetId;
}
// 尝试不同的API路径格式
// 格式1: /v7/airsheet/{file_id}/worksheets/{worksheet_id}
String url = baseUrl + "/airsheet/" + fileId + "/worksheets/" + wsId;
// 根据官方文档https://open.wps.cn/documents/app-integration-dev/wps365/server/airsheet/worksheets/VbHZwButmh
// 正确路径https://openapi.wps.cn/v7/airsheet/{file_id}/worksheets
// 注意:路径中不需要 worksheet_id只需要 file_id
try {
String url = baseUrl + "/airsheet/" + fileId + "/worksheets";
// 如果指定了range添加range参数
// 根据官方文档,可能需要使用 /values 子路径或者使用query参数
if (range != null && !range.trim().isEmpty()) {
// 先尝试作为query参数
url += "?range=" + java.net.URLEncoder.encode(range, "UTF-8");
}
log.debug("读取AirSheet数据 - url: {}, fileId: {}, worksheetId: {}, range: {}", url, fileId, wsId, range);
// 如果指定了worksheetId也可以作为查询参数传递如果API支持
if (worksheetId != null && !worksheetId.trim().isEmpty() && !worksheetId.equals("0") && !worksheetId.equals(fileId)) {
if (url.contains("?")) {
url += "&worksheet_id=" + java.net.URLEncoder.encode(worksheetId, "UTF-8");
} else {
url += "?worksheet_id=" + java.net.URLEncoder.encode(worksheetId, "UTF-8");
}
}
log.debug("使用官方文档路径 - url: {}, fileId: {}, worksheetId: {}, range: {}", url, fileId, worksheetId, range);
return WPS365ApiUtil.httpRequest("GET", url, accessToken, null);
} catch (Exception e) {
log.debug("官方文档路径失败,尝试其他方案", e);
}
// 尝试多种API路径格式降级方案
// 方案1: 尝试使用fileId作为worksheetId如果用户只配置了一个ID
if (wsId.equals("0") && !fileId.equals(worksheetId)) {
try {
String url = baseUrl + "/airsheet/" + fileId + "/worksheets/" + fileId;
if (range != null && !range.trim().isEmpty()) {
url += "?range=" + java.net.URLEncoder.encode(range, "UTF-8");
}
log.debug("尝试方案1 - 使用fileId作为worksheetId - url: {}", url);
return WPS365ApiUtil.httpRequest("GET", url, accessToken, null);
} catch (Exception e) {
log.debug("方案1失败尝试其他方案", e);
}
}
// 方案2: 使用 /range_data 子路径(根据官方文档,这是读取区域数据的标准路径)
// 注意range_data接口需要使用 row_from, row_to, col_from, col_to 参数,而不是 range=A1:B5
try {
String url = baseUrl + "/airsheet/" + fileId + "/worksheets/" + wsId + "/range_data";
if (range != null && !range.trim().isEmpty()) {
// 尝试解析A1:B5格式的range转换为行列参数
int[] rangeParams = parseRangeToRowCol(range);
if (rangeParams != null && rangeParams.length == 4) {
// 使用行列参数格式row_from, row_to, col_from, col_to
// 注意WPS365的行列索引可能从0开始或从1开始需要测试确认
url += "?row_from=" + rangeParams[0] + "&row_to=" + rangeParams[1]
+ "&col_from=" + rangeParams[2] + "&col_to=" + rangeParams[3];
} else {
// 如果解析失败尝试作为range参数传递
url += "?range=" + java.net.URLEncoder.encode(range, "UTF-8");
}
}
log.debug("尝试方案2 - 使用/range_data子路径 - url: {}", url);
return WPS365ApiUtil.httpRequest("GET", url, accessToken, null);
} catch (Exception e) {
log.debug("方案2失败尝试其他方案", e);
}
// 方案3: 使用 /values 子路径
try {
String url = baseUrl + "/airsheet/" + fileId + "/worksheets/" + wsId + "/values";
if (range != null && !range.trim().isEmpty()) {
url += "?range=" + java.net.URLEncoder.encode(range, "UTF-8");
}
log.debug("尝试方案3 - 使用/values子路径 - url: {}", url);
return WPS365ApiUtil.httpRequest("GET", url, accessToken, null);
} catch (Exception e) {
log.debug("方案3失败尝试其他方案", e);
}
// 方案4: 基础路径(不带子路径)
String url = baseUrl + "/airsheet/" + fileId + "/worksheets/" + wsId;
if (range != null && !range.trim().isEmpty()) {
url += "?range=" + java.net.URLEncoder.encode(range, "UTF-8");
}
log.debug("尝试方案4 - 基础路径 - url: {}", url);
JSONObject result = WPS365ApiUtil.httpRequest("GET", url, accessToken, null);
return result;
} catch (Exception e) {
@@ -274,19 +349,13 @@ public class WPS365ApiServiceImpl implements IWPS365ApiService {
public JSONObject updateAirSheetCells(String accessToken, String fileId, String worksheetId, String range, List<List<Object>> values) {
try {
// WPS365 AirSheet API路径格式
// 注意AirSheet中fileId和worksheetId可能是同一个值
// 根据文档https://open.wps.cn/documents/app-integration-dev/wps365/server/airsheet/worksheets/VbHZwButmh
// 正确路径https://openapi.wps.cn/v7/airsheet/{file_id}/worksheets
// 注意:路径中不需要 worksheet_id只需要 file_id
String baseUrl = "https://openapi.wps.cn/v7";
// 如果worksheetId为空或与fileId相同说明用户只配置了一个ID
String wsId;
if (worksheetId == null || worksheetId.trim().isEmpty() || worksheetId.equals(fileId)) {
wsId = fileId;
} else {
wsId = worksheetId;
}
// 尝试不同的API路径格式
String url = baseUrl + "/airsheet/" + fileId + "/worksheets/" + wsId;
// 使用官方文档中的正确路径格式
String url = baseUrl + "/airsheet/" + fileId + "/worksheets";
// 构建请求体
JSONObject requestBody = new JSONObject();
@@ -309,23 +378,106 @@ public class WPS365ApiServiceImpl implements IWPS365ApiService {
}
requestBody.put("values", valuesArray);
// 如果指定了worksheetId也可以添加到请求体中如果API支持
if (worksheetId != null && !worksheetId.trim().isEmpty() && !worksheetId.equals("0") && !worksheetId.equals(fileId)) {
requestBody.put("worksheet_id", worksheetId);
}
String bodyStr = requestBody.toJSONString();
log.debug("更新AirSheet数据 - url: {}, fileId: {}, worksheetId: {}, range: {}, values: {}",
url, fileId, wsId, range, bodyStr);
url, fileId, worksheetId, range, bodyStr);
try {
return WPS365ApiUtil.httpRequest("PUT", url, accessToken, bodyStr);
} catch (Exception e) {
// 如果失败,尝试使用 /values 子路径
log.warn("使用基础路径失败,尝试/values子路径", e);
String urlWithValues = baseUrl + "/airsheet/" + fileId + "/worksheets/" + wsId + "/values";
log.debug("尝试使用/values子路径 - url: {}", urlWithValues);
return WPS365ApiUtil.httpRequest("PUT", urlWithValues, accessToken, bodyStr);
}
} catch (Exception e) {
log.error("更新AirSheet数据失败 - fileId: {}, worksheetId: {}, range: {}", fileId, worksheetId, range, e);
throw new RuntimeException("更新AirSheet数据失败: " + e.getMessage(), e);
}
}
/**
* 解析A1:B5格式的range转换为行列参数
* 返回数组:[row_from, row_to, col_from, col_to]
* 注意WPS365的行列索引可能从0开始或从1开始这里假设从1开始Excel标准
*
* @param range 单元格范围,如 "A1:B5"
* @return 行列参数数组如果解析失败返回null
*/
private int[] parseRangeToRowCol(String range) {
if (range == null || range.trim().isEmpty()) {
return null;
}
try {
// 解析A1:B5格式
String[] parts = range.split(":");
if (parts.length != 2) {
return null;
}
String startCell = parts[0].trim();
String endCell = parts[1].trim();
// 解析起始单元格,如 "A1" -> row=1, col=1
int[] start = parseCellAddress(startCell);
int[] end = parseCellAddress(endCell);
if (start == null || end == null) {
return null;
}
// 返回 [row_from, row_to, col_from, col_to]
// 注意WPS365可能从0开始索引这里先使用从1开始的索引Excel标准
// 如果API要求从0开始需要减1
return new int[]{start[0], end[0], start[1], end[1]};
} catch (Exception e) {
log.warn("解析range失败: {}", range, e);
return null;
}
}
/**
* 解析单元格地址,如 "A1" -> [row=1, col=1]
*
* @param cellAddress 单元格地址,如 "A1", "B5"
* @return [row, col] 数组如果解析失败返回null
*/
private int[] parseCellAddress(String cellAddress) {
if (cellAddress == null || cellAddress.trim().isEmpty()) {
return null;
}
try {
// 分离字母部分(列)和数字部分(行)
// 例如 "A1" -> col="A", row="1"
String colStr = "";
String rowStr = "";
for (char c : cellAddress.toCharArray()) {
if (Character.isLetter(c)) {
colStr += c;
} else if (Character.isDigit(c)) {
rowStr += c;
}
}
if (colStr.isEmpty() || rowStr.isEmpty()) {
return null;
}
// 转换列字母为数字A=1, B=2, ..., Z=26, AA=27, ...
int col = 0;
for (char c : colStr.toUpperCase().toCharArray()) {
col = col * 26 + (c - 'A' + 1);
}
// 转换行号为整数
int row = Integer.parseInt(rowStr);
return new int[]{row, col};
} catch (Exception e) {
log.warn("解析单元格地址失败: {}", cellAddress, e);
return null;
}
}
}

View File

@@ -138,6 +138,14 @@ public class WPS365OAuthServiceImpl implements IWPS365OAuthService {
authUrl.append("&state=").append(state);
log.debug("授权URL参数 - state: {}", state);
// prompt参数可选用于控制授权页面显示
// prompt=consent: 强制显示授权确认页面,即使用户已授权过
// prompt=login: 强制显示登录页面
// 如果不添加此参数,已登录且已授权的用户会直接跳过授权页面
// 注意WPS365可能不支持此参数如果不支持会被忽略
authUrl.append("&prompt=consent");
log.debug("授权URL参数 - prompt: consent (强制显示授权确认页面)");
String result = authUrl.toString();
log.info("生成授权URL: {}", result);
log.warn("⚠️ 请确保WPS365开放平台配置的回调地址与以下地址完全一致包括协议、域名、路径:");
@@ -148,6 +156,10 @@ public class WPS365OAuthServiceImpl implements IWPS365OAuthService {
log.info(" - response_type: code");
log.info(" - scope: {}", scope);
log.info(" - state: {}", state);
log.info(" - prompt: consent (强制显示授权确认页面)");
log.info("💡 说明:已添加 prompt=consent 参数,强制显示授权确认页面");
log.info(" 如果用户已登录且已授权过WPS365可能会跳过授权页面直接返回code");
log.info(" 这是正常的OAuth2行为不是安全问题");
log.info("如果仍然报错,请检查:");
log.info(" 1. WPS365平台配置的回调地址是否与上述redirect_uri完全一致");
log.info(" 2. 参数名是否正确WPS365可能使用app_id而不是client_id");

View File

@@ -29,12 +29,19 @@
<result property="refundReceivedDate" column="refund_received_date"/>
<result property="isRebateReceived" column="is_rebate_received"/>
<result property="rebateReceivedDate" column="rebate_received_date"/>
<result property="isPriceProtected" column="is_price_protected"/>
<result property="priceProtectedDate" column="price_protected_date"/>
<result property="isInvoiceOpened" column="is_invoice_opened"/>
<result property="invoiceOpenedDate" column="invoice_opened_date"/>
<result property="isReviewPosted" column="is_review_posted"/>
<result property="reviewPostedDate" column="review_posted_date"/>
</resultMap>
<sql id="selectJDOrderBase">
select id, remark, distribution_mark, model_number, link, payment_amount, rebate_amount,
address, logistics_link, order_id, buyer, order_time, create_time, update_time, status, is_count_enabled, third_party_order_no, jingfen_actual_price,
is_refunded, refund_date, is_refund_received, refund_received_date, is_rebate_received, rebate_received_date
is_refunded, refund_date, is_refund_received, refund_received_date, is_rebate_received, rebate_received_date,
is_price_protected, price_protected_date, is_invoice_opened, invoice_opened_date, is_review_posted, review_posted_date
from jd_order
</sql>
@@ -62,6 +69,9 @@
<if test="isRefunded != null"> and is_refunded = #{isRefunded}</if>
<if test="isRefundReceived != null"> and is_refund_received = #{isRefundReceived}</if>
<if test="isRebateReceived != null"> and is_rebate_received = #{isRebateReceived}</if>
<if test="isPriceProtected != null"> and is_price_protected = #{isPriceProtected}</if>
<if test="isInvoiceOpened != null"> and is_invoice_opened = #{isInvoiceOpened}</if>
<if test="isReviewPosted != null"> and is_review_posted = #{isReviewPosted}</if>
<if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->
and date(order_time) &gt;= #{params.beginTime}
</if>
@@ -96,6 +106,9 @@
<if test="isRefunded != null"> and is_refunded = #{isRefunded}</if>
<if test="isRefundReceived != null"> and is_refund_received = #{isRefundReceived}</if>
<if test="isRebateReceived != null"> and is_rebate_received = #{isRebateReceived}</if>
<if test="isPriceProtected != null"> and is_price_protected = #{isPriceProtected}</if>
<if test="isInvoiceOpened != null"> and is_invoice_opened = #{isInvoiceOpened}</if>
<if test="isReviewPosted != null"> and is_review_posted = #{isReviewPosted}</if>
<if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->
and date(order_time) &gt;= #{params.beginTime}
</if>
@@ -126,13 +139,15 @@
payment_amount, rebate_amount, address, logistics_link,
tencent_doc_pushed, tencent_doc_push_time,
order_id, buyer, order_time, create_time, update_time, status, is_count_enabled, third_party_order_no, jingfen_actual_price,
is_refunded, refund_date, is_refund_received, refund_received_date, is_rebate_received, rebate_received_date
is_refunded, refund_date, is_refund_received, refund_received_date, is_rebate_received, rebate_received_date,
is_price_protected, price_protected_date, is_invoice_opened, invoice_opened_date, is_review_posted, review_posted_date
) values (
#{remark}, #{distributionMark}, #{modelNumber}, #{link},
#{paymentAmount}, #{rebateAmount}, #{address}, #{logisticsLink},
0, null,
#{orderId}, #{buyer}, #{orderTime}, now(), now(), #{status}, #{isCountEnabled}, #{thirdPartyOrderNo}, #{jingfenActualPrice},
#{isRefunded}, #{refundDate}, #{isRefundReceived}, #{refundReceivedDate}, #{isRebateReceived}, #{rebateReceivedDate}
#{isRefunded}, #{refundDate}, #{isRefundReceived}, #{refundReceivedDate}, #{isRebateReceived}, #{rebateReceivedDate},
#{isPriceProtected}, #{priceProtectedDate}, #{isInvoiceOpened}, #{invoiceOpenedDate}, #{isReviewPosted}, #{reviewPostedDate}
)
</insert>
@@ -162,6 +177,12 @@
<if test="refundReceivedDate != null"> refund_received_date = #{refundReceivedDate},</if>
<if test="isRebateReceived != null"> is_rebate_received = #{isRebateReceived},</if>
<if test="rebateReceivedDate != null"> rebate_received_date = #{rebateReceivedDate},</if>
<if test="isPriceProtected != null"> is_price_protected = #{isPriceProtected},</if>
<if test="priceProtectedDate != null"> price_protected_date = #{priceProtectedDate},</if>
<if test="isInvoiceOpened != null"> is_invoice_opened = #{isInvoiceOpened},</if>
<if test="invoiceOpenedDate != null"> invoice_opened_date = #{invoiceOpenedDate},</if>
<if test="isReviewPosted != null"> is_review_posted = #{isReviewPosted},</if>
<if test="reviewPostedDate != null"> review_posted_date = #{reviewPostedDate},</if>
update_time = now()
</set>
where id = #{id}

View File

@@ -0,0 +1,15 @@
-- 为jd_order表添加新的状态字段点过价保、开过专票、晒过评价
-- 执行日期2025-01-26
ALTER TABLE jd_order
ADD COLUMN is_price_protected INT DEFAULT 0 COMMENT '点过价保0否 1是',
ADD COLUMN price_protected_date DATETIME NULL COMMENT '价保日期',
ADD COLUMN is_invoice_opened INT DEFAULT 0 COMMENT '开过专票0否 1是',
ADD COLUMN invoice_opened_date DATETIME NULL COMMENT '开票日期',
ADD COLUMN is_review_posted INT DEFAULT 0 COMMENT '晒过评价0否 1是',
ADD COLUMN review_posted_date DATETIME NULL COMMENT '评价日期';
-- 添加索引(可选,根据查询需求)
-- CREATE INDEX idx_is_price_protected ON jd_order(is_price_protected);
-- CREATE INDEX idx_is_invoice_opened ON jd_order(is_invoice_opened);
-- CREATE INDEX idx_is_review_posted ON jd_order(is_review_posted);