Commit bff07b1f020dda8518e3f1d38ad8b038f3fa053d
1 parent
c6d1116d
Refactor FileVariable and LqEventService to include UserSignatureFilePath; enhan…
…ce LqEventUser DTO with StoreId; implement Excel import functionality for LqEvent users; add UploadBase64Image method in FileService for user signature uploads.
Showing
13 changed files
with
610 additions
and
38 deletions
netcore/src/Modularity/Common/NCC.Common/Configuration/FileVariable.cs
| 1 | -using NCC.Dependency; | |
| 2 | -using System.IO; | |
| 1 | +using System.IO; | |
| 2 | +using NCC.Dependency; | |
| 3 | 3 | |
| 4 | 4 | namespace NCC.Common.Configuration |
| 5 | 5 | { |
| ... | ... | @@ -73,5 +73,10 @@ namespace NCC.Common.Configuration |
| 73 | 73 | /// 模板路径 |
| 74 | 74 | /// </summary> |
| 75 | 75 | public static string TemplateFilePath = SystemPath + "TemplateFile/"; |
| 76 | + | |
| 77 | + /// <summary> | |
| 78 | + /// 用户签名存储路径 | |
| 79 | + /// </summary> | |
| 80 | + public static string UserSignatureFilePath = SystemPath + "UserSignature/"; | |
| 76 | 81 | } |
| 77 | 82 | } | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqEvent/LqEventImportResult.cs
0 → 100644
| 1 | +using System.Collections.Generic; | |
| 2 | +using NCC.Extend.Entitys.Dto.LqEventUser; | |
| 3 | + | |
| 4 | +namespace NCC.Extend.Entitys.Dto.LqEvent | |
| 5 | +{ | |
| 6 | + /// <summary> | |
| 7 | + /// 拓客活动Excel导入结果 | |
| 8 | + /// </summary> | |
| 9 | + public class LqEventImportResult | |
| 10 | + { | |
| 11 | + /// <summary> | |
| 12 | + /// 成功数量 | |
| 13 | + /// </summary> | |
| 14 | + public int SuccessCount { get; set; } | |
| 15 | + | |
| 16 | + /// <summary> | |
| 17 | + /// 失败数量 | |
| 18 | + /// </summary> | |
| 19 | + public int FailCount { get; set; } | |
| 20 | + | |
| 21 | + /// <summary> | |
| 22 | + /// 总数量 | |
| 23 | + /// </summary> | |
| 24 | + public int TotalCount { get; set; } | |
| 25 | + | |
| 26 | + /// <summary> | |
| 27 | + /// 成功的数据列表 | |
| 28 | + /// </summary> | |
| 29 | + public List<LqEventUserCrInput> SuccessData { get; set; } = new List<LqEventUserCrInput>(); | |
| 30 | + | |
| 31 | + /// <summary> | |
| 32 | + /// 失败的数据列表 | |
| 33 | + /// </summary> | |
| 34 | + public List<LqEventImportError> FailData { get; set; } = new List<LqEventImportError>(); | |
| 35 | + } | |
| 36 | + | |
| 37 | + /// <summary> | |
| 38 | + /// 拓客活动导入错误信息 | |
| 39 | + /// </summary> | |
| 40 | + public class LqEventImportError | |
| 41 | + { | |
| 42 | + /// <summary> | |
| 43 | + /// 行号 | |
| 44 | + /// </summary> | |
| 45 | + public int RowNumber { get; set; } | |
| 46 | + | |
| 47 | + /// <summary> | |
| 48 | + /// 员工手机号 | |
| 49 | + /// </summary> | |
| 50 | + public string MobilePhone { get; set; } | |
| 51 | + | |
| 52 | + /// <summary> | |
| 53 | + /// 姓名 | |
| 54 | + /// </summary> | |
| 55 | + public string Name { get; set; } | |
| 56 | + | |
| 57 | + /// <summary> | |
| 58 | + /// 战队 | |
| 59 | + /// </summary> | |
| 60 | + public string TeamName { get; set; } | |
| 61 | + | |
| 62 | + /// <summary> | |
| 63 | + /// 门店 | |
| 64 | + /// </summary> | |
| 65 | + public string StoreName { get; set; } | |
| 66 | + | |
| 67 | + /// <summary> | |
| 68 | + /// 目标张数 | |
| 69 | + /// </summary> | |
| 70 | + public int? TargetCount { get; set; } | |
| 71 | + | |
| 72 | + /// <summary> | |
| 73 | + /// 错误信息 | |
| 74 | + /// </summary> | |
| 75 | + public string ErrorMessage { get; set; } | |
| 76 | + } | |
| 77 | +} | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqEventUser/LqEventUserCrInput.cs
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKdPxmx/LqKdPxmxInfoOutput.cs
| ... | ... | @@ -12,62 +12,67 @@ namespace NCC.Extend.Entitys.Dto.LqKdKdjlb |
| 12 | 12 | /// 明细编号 |
| 13 | 13 | /// </summary> |
| 14 | 14 | public string id { get; set; } |
| 15 | - | |
| 15 | + | |
| 16 | 16 | /// <summary> |
| 17 | 17 | /// 关联开单编号 |
| 18 | 18 | /// </summary> |
| 19 | 19 | public string glkdbh { get; set; } |
| 20 | - | |
| 20 | + | |
| 21 | 21 | /// <summary> |
| 22 | 22 | /// 品项编号 |
| 23 | 23 | /// </summary> |
| 24 | 24 | public string px { get; set; } |
| 25 | - | |
| 25 | + | |
| 26 | 26 | /// <summary> |
| 27 | 27 | /// 品项名称 |
| 28 | 28 | /// </summary> |
| 29 | 29 | public string pxmc { get; set; } |
| 30 | - | |
| 30 | + | |
| 31 | 31 | /// <summary> |
| 32 | 32 | /// 品项价格 |
| 33 | 33 | /// </summary> |
| 34 | 34 | public decimal pxjg { get; set; } |
| 35 | - | |
| 35 | + | |
| 36 | 36 | /// <summary> |
| 37 | 37 | /// 项目次数 |
| 38 | 38 | /// </summary> |
| 39 | 39 | public int? projectNumber { get; set; } |
| 40 | - | |
| 40 | + | |
| 41 | 41 | /// <summary> |
| 42 | 42 | /// 是否有效 |
| 43 | 43 | /// </summary> |
| 44 | 44 | public int? isEnabled { get; set; } |
| 45 | - | |
| 45 | + | |
| 46 | 46 | /// <summary> |
| 47 | 47 | /// 来源类型(开卡/赠送/其他) |
| 48 | 48 | /// </summary> |
| 49 | 49 | public string sourceType { get; set; } |
| 50 | - | |
| 50 | + | |
| 51 | 51 | /// <summary> |
| 52 | 52 | /// 会员ID |
| 53 | 53 | /// </summary> |
| 54 | 54 | public string memberId { get; set; } |
| 55 | - | |
| 55 | + | |
| 56 | 56 | /// <summary> |
| 57 | 57 | /// 创建时间 |
| 58 | 58 | /// </summary> |
| 59 | 59 | public DateTime? createTime { get; set; } |
| 60 | - | |
| 60 | + | |
| 61 | 61 | /// <summary> |
| 62 | 62 | /// 总价格(品项价格 × 项目次数) |
| 63 | 63 | /// </summary> |
| 64 | 64 | public decimal totalPrice { get; set; } |
| 65 | - | |
| 65 | + | |
| 66 | + /// <summary> | |
| 67 | + /// 实际价格 | |
| 68 | + /// </summary> | |
| 69 | + public decimal actualPrice { get; set; } | |
| 70 | + | |
| 66 | 71 | /// <summary> |
| 67 | 72 | /// 健康师业绩列表 |
| 68 | 73 | /// </summary> |
| 69 | 74 | public List<LqKdJksyjInfoOutput> lqKdJksyjList { get; set; } |
| 70 | - | |
| 75 | + | |
| 71 | 76 | /// <summary> |
| 72 | 77 | /// 科技部老师业绩列表 |
| 73 | 78 | /// </summary> | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/LqKhxx/LqKhxxCrInput.cs
netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/lq_eventuser/LqEventUserEntity.cs
| ... | ... | @@ -56,5 +56,11 @@ namespace NCC.Extend.Entitys.lq_eventuser |
| 56 | 56 | /// </summary> |
| 57 | 57 | [SugarColumn(ColumnName = "F_EventTarget")] |
| 58 | 58 | public int EventTarget { get; set; } |
| 59 | + | |
| 60 | + /// <summary> | |
| 61 | + /// 门店id | |
| 62 | + /// </summary> | |
| 63 | + [SugarColumn(ColumnName = "F_StoreId")] | |
| 64 | + public string StoreId { get; set; } | |
| 59 | 65 | } |
| 60 | 66 | } | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend/LqEventService.cs
| 1 | 1 | using System; |
| 2 | 2 | using System.Collections.Generic; |
| 3 | +using System.IO; | |
| 3 | 4 | using System.Linq; |
| 4 | 5 | using System.Threading.Tasks; |
| 5 | 6 | using Mapster; |
| 7 | +using Microsoft.AspNetCore.Http; | |
| 6 | 8 | using Microsoft.AspNetCore.Mvc; |
| 7 | 9 | using NCC.Common.Core.Manager; |
| 8 | 10 | using NCC.Common.Filter; |
| 11 | +using NCC.Common.Helper; | |
| 9 | 12 | using NCC.Dependency; |
| 10 | 13 | using NCC.DynamicApiController; |
| 11 | 14 | using NCC.Extend.Entitys.Dto.LqEvent; |
| 12 | 15 | using NCC.Extend.Entitys.Dto.LqEventUser; |
| 13 | 16 | using NCC.Extend.Entitys.lq_event; |
| 14 | 17 | using NCC.Extend.Entitys.lq_eventuser; |
| 18 | +using NCC.Extend.Entitys.lq_mdxx; | |
| 15 | 19 | using NCC.Extend.Interfaces.LqEvent; |
| 16 | 20 | using NCC.FriendlyException; |
| 21 | +using NCC.System.Entitys.Permission; | |
| 17 | 22 | using SqlSugar; |
| 18 | 23 | using Yitter.IdGenerator; |
| 19 | 24 | |
| ... | ... | @@ -219,6 +224,7 @@ namespace NCC.Extend.LqEvent |
| 219 | 224 | TeamName = member.TeamName, |
| 220 | 225 | CreationTime = DateTime.Now, |
| 221 | 226 | CreationUser = userInfo?.userName, |
| 227 | + StoreId = member.StoreId, | |
| 222 | 228 | } |
| 223 | 229 | ); |
| 224 | 230 | } |
| ... | ... | @@ -306,6 +312,7 @@ namespace NCC.Extend.LqEvent |
| 306 | 312 | TeamName = member.TeamName, |
| 307 | 313 | CreationTime = DateTime.Now, |
| 308 | 314 | CreationUser = userInfo?.userName, |
| 315 | + StoreId = member.StoreId, | |
| 309 | 316 | } |
| 310 | 317 | ); |
| 311 | 318 | } |
| ... | ... | @@ -482,5 +489,235 @@ namespace NCC.Extend.LqEvent |
| 482 | 489 | } |
| 483 | 490 | } |
| 484 | 491 | #endregion |
| 492 | + | |
| 493 | + #region Excel导入拓客活动用户 | |
| 494 | + | |
| 495 | + /// <summary> | |
| 496 | + /// Excel导入拓客活动用户 | |
| 497 | + /// </summary> | |
| 498 | + /// <remarks> | |
| 499 | + /// 通过Excel文件导入拓客活动用户数据,支持批量导入。 | |
| 500 | + /// Excel列名:员工手机号、姓名、战队、门店、目标张数 | |
| 501 | + /// | |
| 502 | + /// 导入流程: | |
| 503 | + /// 1. 通过员工手机号查找用户信息,获取用户ID和组织ID | |
| 504 | + /// 2. 通过门店中文名称查找门店信息,获取门店ID | |
| 505 | + /// 3. 验证数据完整性 | |
| 506 | + /// 4. 返回成功和失败的数据列表 | |
| 507 | + /// | |
| 508 | + /// 示例请求: | |
| 509 | + /// POST /api/Extend/LqEvent/import-users | |
| 510 | + /// Content-Type: multipart/form-data | |
| 511 | + /// | |
| 512 | + /// 参数: | |
| 513 | + /// - file: Excel文件 | |
| 514 | + /// | |
| 515 | + /// 返回数据: | |
| 516 | + /// - SuccessCount: 成功数量 | |
| 517 | + /// - FailCount: 失败数量 | |
| 518 | + /// - SuccessData: 成功的数据列表 | |
| 519 | + /// - FailData: 失败的数据列表(包含错误信息) | |
| 520 | + /// </remarks> | |
| 521 | + /// <param name="file">Excel文件</param> | |
| 522 | + /// <param name="eventId">拓客活动ID</param> | |
| 523 | + /// <returns>导入结果</returns> | |
| 524 | + /// <response code="200">导入完成,返回成功和失败的数据统计</response> | |
| 525 | + /// <response code="400">请求参数错误或文件格式不正确</response> | |
| 526 | + /// <response code="500">服务器内部错误</response> | |
| 527 | + [HttpPost("import-users")] | |
| 528 | + public async Task<LqEventImportResult> ImportUsers(IFormFile file) | |
| 529 | + { | |
| 530 | + try | |
| 531 | + { | |
| 532 | + if (file == null || file.Length == 0) | |
| 533 | + { | |
| 534 | + throw NCCException.Oh("请选择要导入的Excel文件"); | |
| 535 | + } | |
| 536 | + var result = new LqEventImportResult(); | |
| 537 | + // 读取Excel文件 | |
| 538 | + var excelData = await ReadExcelFile(file); | |
| 539 | + result.TotalCount = excelData.Count; | |
| 540 | + | |
| 541 | + foreach (var row in excelData) | |
| 542 | + { | |
| 543 | + try | |
| 544 | + { | |
| 545 | + var importError = new LqEventImportError | |
| 546 | + { | |
| 547 | + RowNumber = row.RowNumber, | |
| 548 | + MobilePhone = row.MobilePhone, | |
| 549 | + Name = row.Name, | |
| 550 | + TeamName = row.TeamName, | |
| 551 | + StoreName = row.StoreName, | |
| 552 | + TargetCount = row.TargetCount, | |
| 553 | + }; | |
| 554 | + | |
| 555 | + // 验证必填字段 | |
| 556 | + if (string.IsNullOrEmpty(row.MobilePhone)) | |
| 557 | + { | |
| 558 | + importError.ErrorMessage = "员工手机号不能为空"; | |
| 559 | + result.FailData.Add(importError); | |
| 560 | + result.FailCount++; | |
| 561 | + continue; | |
| 562 | + } | |
| 563 | + | |
| 564 | + // 通过手机号查找用户 | |
| 565 | + var user = await _db.Queryable<UserEntity>().Where(u => u.MobilePhone == row.MobilePhone).FirstAsync(); | |
| 566 | + | |
| 567 | + if (user == null) | |
| 568 | + { | |
| 569 | + importError.ErrorMessage = $"未找到手机号为 {row.MobilePhone} 的用户"; | |
| 570 | + result.FailData.Add(importError); | |
| 571 | + result.FailCount++; | |
| 572 | + continue; | |
| 573 | + } | |
| 574 | + | |
| 575 | + // 获取用户组织ID(部门ID) | |
| 576 | + if (string.IsNullOrEmpty(user.OrganizeId)) | |
| 577 | + { | |
| 578 | + importError.ErrorMessage = $"用户 {user.RealName} 没有关联的组织信息"; | |
| 579 | + result.FailData.Add(importError); | |
| 580 | + result.FailCount++; | |
| 581 | + continue; | |
| 582 | + } | |
| 583 | + | |
| 584 | + // 查找门店信息 | |
| 585 | + string storeId = null; | |
| 586 | + if (!string.IsNullOrEmpty(row.StoreName)) | |
| 587 | + { | |
| 588 | + var store = await _db.Queryable<LqMdxxEntity>().Where(s => s.Dm == row.StoreName).FirstAsync(); | |
| 589 | + | |
| 590 | + if (store == null) | |
| 591 | + { | |
| 592 | + importError.ErrorMessage = $"未找到名称为 {row.StoreName} 的门店"; | |
| 593 | + result.FailData.Add(importError); | |
| 594 | + result.FailCount++; | |
| 595 | + continue; | |
| 596 | + } | |
| 597 | + storeId = store.Id; | |
| 598 | + } | |
| 599 | + | |
| 600 | + // 创建成功的数据 | |
| 601 | + var successData = new NCC.Extend.Entitys.Dto.LqEventUser.LqEventUserCrInput | |
| 602 | + { | |
| 603 | + Id = YitIdHelper.NextId().ToString(), | |
| 604 | + EventId = eventId, | |
| 605 | + UserId = user.Id, | |
| 606 | + DepId = user.OrganizeId, | |
| 607 | + TeamName = row.TeamName, | |
| 608 | + StoreId = storeId, | |
| 609 | + }; | |
| 610 | + | |
| 611 | + result.SuccessData.Add(successData); | |
| 612 | + result.SuccessCount++; | |
| 613 | + } | |
| 614 | + catch (Exception ex) | |
| 615 | + { | |
| 616 | + var importError = new LqEventImportError | |
| 617 | + { | |
| 618 | + RowNumber = row.RowNumber, | |
| 619 | + MobilePhone = row.MobilePhone, | |
| 620 | + Name = row.Name, | |
| 621 | + TeamName = row.TeamName, | |
| 622 | + StoreName = row.StoreName, | |
| 623 | + TargetCount = row.TargetCount, | |
| 624 | + ErrorMessage = $"处理数据时发生错误: {ex.Message}", | |
| 625 | + }; | |
| 626 | + result.FailData.Add(importError); | |
| 627 | + result.FailCount++; | |
| 628 | + } | |
| 629 | + } | |
| 630 | + | |
| 631 | + return result; | |
| 632 | + } | |
| 633 | + catch (Exception ex) | |
| 634 | + { | |
| 635 | + throw NCCException.Oh($"导入拓客活动用户失败: {ex.Message}", ex); | |
| 636 | + } | |
| 637 | + } | |
| 638 | + | |
| 639 | + /// <summary> | |
| 640 | + /// 读取Excel文件数据 | |
| 641 | + /// </summary> | |
| 642 | + /// <param name="file">Excel文件</param> | |
| 643 | + /// <returns>Excel数据列表</returns> | |
| 644 | + [NonAction] | |
| 645 | + private async Task<List<ExcelImportRow>> ReadExcelFile(IFormFile file) | |
| 646 | + { | |
| 647 | + var result = new List<ExcelImportRow>(); | |
| 648 | + | |
| 649 | + // 保存临时文件 | |
| 650 | + var tempFilePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + Path.GetExtension(file.FileName)); | |
| 651 | + try | |
| 652 | + { | |
| 653 | + using (var stream = new FileStream(tempFilePath, FileMode.Create)) | |
| 654 | + { | |
| 655 | + await file.CopyToAsync(stream); | |
| 656 | + } | |
| 657 | + | |
| 658 | + // 使用ExcelImportHelper读取Excel文件 | |
| 659 | + var dataTable = ExcelImportHelper.ToDataTable(tempFilePath, 0, 0); | |
| 660 | + | |
| 661 | + // 从第2行开始读取数据(第1行是标题) | |
| 662 | + for (int i = 1; i < dataTable.Rows.Count; i++) | |
| 663 | + { | |
| 664 | + var row = dataTable.Rows[i]; | |
| 665 | + var mobilePhone = row[0]?.ToString()?.Trim(); | |
| 666 | + var name = row[1]?.ToString()?.Trim(); | |
| 667 | + var teamName = row[2]?.ToString()?.Trim(); | |
| 668 | + var storeName = row[3]?.ToString()?.Trim(); | |
| 669 | + var targetCountStr = row[4]?.ToString()?.Trim(); | |
| 670 | + | |
| 671 | + int? targetCount = null; | |
| 672 | + if (!string.IsNullOrEmpty(targetCountStr) && int.TryParse(targetCountStr, out int target)) | |
| 673 | + { | |
| 674 | + targetCount = target; | |
| 675 | + } | |
| 676 | + | |
| 677 | + // 跳过空行 | |
| 678 | + if (string.IsNullOrEmpty(mobilePhone) && string.IsNullOrEmpty(name)) | |
| 679 | + { | |
| 680 | + continue; | |
| 681 | + } | |
| 682 | + | |
| 683 | + result.Add( | |
| 684 | + new ExcelImportRow | |
| 685 | + { | |
| 686 | + RowNumber = i + 1, // Excel行号从1开始 | |
| 687 | + MobilePhone = mobilePhone, | |
| 688 | + Name = name, | |
| 689 | + TeamName = teamName, | |
| 690 | + StoreName = storeName, | |
| 691 | + TargetCount = targetCount, | |
| 692 | + } | |
| 693 | + ); | |
| 694 | + } | |
| 695 | + } | |
| 696 | + finally | |
| 697 | + { | |
| 698 | + // 清理临时文件 | |
| 699 | + if (File.Exists(tempFilePath)) | |
| 700 | + { | |
| 701 | + File.Delete(tempFilePath); | |
| 702 | + } | |
| 703 | + } | |
| 704 | + | |
| 705 | + return result; | |
| 706 | + } | |
| 707 | + | |
| 708 | + /// <summary> | |
| 709 | + /// Excel导入行数据 | |
| 710 | + /// </summary> | |
| 711 | + private class ExcelImportRow | |
| 712 | + { | |
| 713 | + public int RowNumber { get; set; } | |
| 714 | + public string MobilePhone { get; set; } | |
| 715 | + public string Name { get; set; } | |
| 716 | + public string TeamName { get; set; } | |
| 717 | + public string StoreName { get; set; } | |
| 718 | + public int? TargetCount { get; set; } | |
| 719 | + } | |
| 720 | + | |
| 721 | + #endregion | |
| 485 | 722 | } |
| 486 | 723 | } | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend/LqKdKdjlbService.cs
netcore/src/Modularity/Extend/NCC.Extend/LqKhxxService.cs
| ... | ... | @@ -157,6 +157,13 @@ namespace NCC.Extend.LqKhxx |
| 157 | 157 | } |
| 158 | 158 | var entity = input.Adapt<LqKhxxEntity>(); |
| 159 | 159 | entity.Id = YitIdHelper.NextId().ToString(); |
| 160 | + | |
| 161 | + // 处理客户消费字段:将数组转换为逗号分隔的字符串 | |
| 162 | + if (input.khxf != null && input.khxf.Count > 0) | |
| 163 | + { | |
| 164 | + entity.Khxf = string.Join(",", input.khxf); | |
| 165 | + } | |
| 166 | + | |
| 160 | 167 | var isOk = await _db.Insertable(entity).IgnoreColumns(ignoreNullColumn: true).ExecuteCommandAsync(); |
| 161 | 168 | if (!(isOk > 0)) |
| 162 | 169 | throw NCCException.Oh(ErrorCode.COM1000); |
| ... | ... | @@ -464,6 +471,13 @@ namespace NCC.Extend.LqKhxx |
| 464 | 471 | public async Task Update(string id, [FromBody] LqKhxxUpInput input) |
| 465 | 472 | { |
| 466 | 473 | var entity = input.Adapt<LqKhxxEntity>(); |
| 474 | + | |
| 475 | + // 处理客户消费字段:将数组转换为逗号分隔的字符串 | |
| 476 | + if (input.khxf != null && input.khxf.Count > 0) | |
| 477 | + { | |
| 478 | + entity.Khxf = string.Join(",", input.khxf); | |
| 479 | + } | |
| 480 | + | |
| 467 | 481 | var isOk = await _db.Updateable(entity).IgnoreColumns(ignoreAllNullColumns: true).ExecuteCommandAsync(); |
| 468 | 482 | if (!(isOk > 0)) |
| 469 | 483 | throw NCCException.Oh(ErrorCode.COM1001); | ... | ... |
netcore/src/Modularity/Extend/NCC.Extend/NCC.Extend.csproj
netcore/src/Modularity/System/NCC.System/Entitys/Dto/Base64ImageUploadInput.cs
0 → 100644
| 1 | +using System.ComponentModel.DataAnnotations; | |
| 2 | + | |
| 3 | +namespace NCC.System.Entitys.Dto | |
| 4 | +{ | |
| 5 | + /// <summary> | |
| 6 | + /// Base64图片上传输入参数 | |
| 7 | + /// </summary> | |
| 8 | + public class Base64ImageUploadInput | |
| 9 | + { | |
| 10 | + /// <summary> | |
| 11 | + /// Base64编码的图片数据 | |
| 12 | + /// 支持格式:data:image/xxx;base64,xxxxx 或纯base64字符串 | |
| 13 | + /// </summary> | |
| 14 | + [Required(ErrorMessage = "Base64数据不能为空")] | |
| 15 | + public string Base64Data { get; set; } | |
| 16 | + | |
| 17 | + /// <summary> | |
| 18 | + /// 图片类型(可选) | |
| 19 | + /// 支持的类型:userAvatar, document, temporary, weixin, workFlow, annex, annexpic, diskdocument, preview, screenShot, banner, bg, border, source, template, codeGenerator | |
| 20 | + /// 默认为 temporary | |
| 21 | + /// </summary> | |
| 22 | + public string ImageType { get; set; } | |
| 23 | + | |
| 24 | + /// <summary> | |
| 25 | + /// 文件名(可选,不包含扩展名) | |
| 26 | + /// 如果不提供,系统会自动生成 | |
| 27 | + /// </summary> | |
| 28 | + public string FileName { get; set; } | |
| 29 | + } | |
| 30 | +} | ... | ... |
netcore/src/Modularity/System/NCC.System/Service/Common/FileService.cs
| 1 | -using NCC.Common.Configuration; | |
| 1 | +using System; | |
| 2 | +using System.IO; | |
| 3 | +using System.Linq; | |
| 4 | +using System.Text; | |
| 5 | +using System.Text.RegularExpressions; | |
| 6 | +using System.Threading.Tasks; | |
| 7 | +using System.Web; | |
| 8 | +using Microsoft.AspNetCore.Authorization; | |
| 9 | +using Microsoft.AspNetCore.Http; | |
| 10 | +using Microsoft.AspNetCore.Mvc; | |
| 11 | +using Microsoft.Extensions.Configuration; | |
| 12 | +using NCC.Common.Configuration; | |
| 2 | 13 | using NCC.Common.Core.Captcha.General; |
| 3 | 14 | using NCC.Common.Core.Manager; |
| 4 | 15 | using NCC.Common.Enum; |
| ... | ... | @@ -10,18 +21,9 @@ using NCC.DynamicApiController; |
| 10 | 21 | using NCC.FriendlyException; |
| 11 | 22 | using NCC.JsonSerialization; |
| 12 | 23 | using NCC.RemoteRequest.Extensions; |
| 24 | +using NCC.System.Entitys.Dto; | |
| 13 | 25 | using NCC.System.Interfaces.Common; |
| 14 | -using Microsoft.AspNetCore.Authorization; | |
| 15 | -using Microsoft.AspNetCore.Http; | |
| 16 | -using Microsoft.AspNetCore.Mvc; | |
| 17 | -using Microsoft.Extensions.Configuration; | |
| 18 | 26 | using OnceMi.AspNetCore.OSS; |
| 19 | -using System; | |
| 20 | -using System.IO; | |
| 21 | -using System.Linq; | |
| 22 | -using System.Text; | |
| 23 | -using System.Threading.Tasks; | |
| 24 | -using System.Web; | |
| 25 | 27 | using Yitter.IdGenerator; |
| 26 | 28 | |
| 27 | 29 | namespace NCC.System.Service.Common |
| ... | ... | @@ -33,7 +35,7 @@ namespace NCC.System.Service.Common |
| 33 | 35 | [Route("api/[controller]")] |
| 34 | 36 | public class FileService : IFileService, IDynamicApiController, ITransient |
| 35 | 37 | { |
| 36 | - private readonly IGeneralCaptcha _captchaHandle;// 验证码服务 | |
| 38 | + private readonly IGeneralCaptcha _captchaHandle; // 验证码服务 | |
| 37 | 39 | private readonly IConfiguration _configuration; |
| 38 | 40 | private readonly IUserManager _userManager; |
| 39 | 41 | private readonly IOSSServiceFactory _oSSServiceFactory; |
| ... | ... | @@ -177,7 +179,8 @@ namespace NCC.System.Service.Common |
| 177 | 179 | var fileDownloadName = exname; |
| 178 | 180 | if (fileDownloadName.IsNullOrWhiteSpace() || fileDownloadName.Split('.').Length < 2) |
| 179 | 181 | fileDownloadName = Path.GetFileName(filePath); |
| 180 | - if (fileDownloadName.IsNullOrWhiteSpace()) fileDownloadName = fileName.Replace(GetPathByType(type), ""); | |
| 182 | + if (fileDownloadName.IsNullOrWhiteSpace()) | |
| 183 | + fileDownloadName = fileName.Replace(GetPathByType(type), ""); | |
| 181 | 184 | return await DownloadFileByType(filePath, fileDownloadName); |
| 182 | 185 | } |
| 183 | 186 | else |
| ... | ... | @@ -282,6 +285,8 @@ namespace NCC.System.Service.Common |
| 282 | 285 | { |
| 283 | 286 | case "userAvatar": |
| 284 | 287 | return FileVariable.UserAvatarFilePath; |
| 288 | + case "userSignature": | |
| 289 | + return FileVariable.UserSignatureFilePath; | |
| 285 | 290 | case "mail": |
| 286 | 291 | return FileVariable.EmailFilePath; |
| 287 | 292 | case "IM": |
| ... | ... | @@ -352,10 +357,6 @@ namespace NCC.System.Service.Common |
| 352 | 357 | return false; |
| 353 | 358 | } |
| 354 | 359 | |
| 355 | - | |
| 356 | - | |
| 357 | - | |
| 358 | - | |
| 359 | 360 | #region 导入导出 |
| 360 | 361 | |
| 361 | 362 | /// <summary> |
| ... | ... | @@ -374,11 +375,7 @@ namespace NCC.System.Service.Common |
| 374 | 375 | var byteList = new UTF8Encoding(true).GetBytes(jsonStr.ToCharArray()); |
| 375 | 376 | FileHelper.CreateFile(_filePath + _fileName, byteList); |
| 376 | 377 | var fileName = _userManager.UserId + "|" + _filePath + _fileName + "|json"; |
| 377 | - var output = new | |
| 378 | - { | |
| 379 | - name = _fileName, | |
| 380 | - url = "/api/file/Download?encryption=" + DESCEncryption.Encrypt(fileName, "NCC") | |
| 381 | - }; | |
| 378 | + var output = new { name = _fileName, url = "/api/file/Download?encryption=" + DESCEncryption.Encrypt(fileName, "NCC") }; | |
| 382 | 379 | return output; |
| 383 | 380 | } |
| 384 | 381 | |
| ... | ... | @@ -422,7 +419,6 @@ namespace NCC.System.Service.Common |
| 422 | 419 | } |
| 423 | 420 | } |
| 424 | 421 | |
| 425 | - | |
| 426 | 422 | /// <summary> |
| 427 | 423 | /// 上传文件 |
| 428 | 424 | /// </summary> |
| ... | ... | @@ -446,5 +442,192 @@ namespace NCC.System.Service.Common |
| 446 | 442 | } |
| 447 | 443 | } |
| 448 | 444 | #endregion |
| 445 | + | |
| 446 | + #region Base64图片上传 | |
| 447 | + | |
| 448 | + /// <summary> | |
| 449 | + /// 上传Base64格式图片(主要用于用户签名信息) | |
| 450 | + /// </summary> | |
| 451 | + /// <remarks> | |
| 452 | + /// 接收前端传入的base64格式图片数据,解码后保存到服务器并返回访问路径。 | |
| 453 | + /// 支持常见的图片格式(JPG、PNG、GIF、BMP等),主要用于保存用户签名信息。 | |
| 454 | + /// | |
| 455 | + /// 示例请求: | |
| 456 | + /// POST /api/File/UploadBase64Image | |
| 457 | + /// Content-Type: application/json | |
| 458 | + /// | |
| 459 | + /// ```json | |
| 460 | + /// { | |
| 461 | + /// "base64Data": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD...", | |
| 462 | + /// "imageType": "userSignature", | |
| 463 | + /// "fileName": "signature" | |
| 464 | + /// } | |
| 465 | + /// ``` | |
| 466 | + /// | |
| 467 | + /// 参数说明: | |
| 468 | + /// - base64Data: 必填,base64编码的图片数据(支持data:image/xxx;base64,格式) | |
| 469 | + /// - imageType: 可选,图片类型(userSignature用于用户签名,默认为temporary) | |
| 470 | + /// - fileName: 可选,文件名(不包含扩展名,系统会自动添加) | |
| 471 | + /// | |
| 472 | + /// 支持的图片类型: | |
| 473 | + /// - userSignature: 用户签名(推荐) | |
| 474 | + /// - userAvatar: 用户头像 | |
| 475 | + /// - document: 文档 | |
| 476 | + /// - temporary: 临时文件(默认) | |
| 477 | + /// - 其他系统支持的图片类型 | |
| 478 | + /// | |
| 479 | + /// 返回数据: | |
| 480 | + /// - name: 保存的文件名 | |
| 481 | + /// - url: 图片访问路径 | |
| 482 | + /// - fileSize: 文件大小(字节) | |
| 483 | + /// - imageFormat: 图片格式 | |
| 484 | + /// - imageType: 图片类型 | |
| 485 | + /// </remarks> | |
| 486 | + /// <param name="input">Base64图片上传参数</param> | |
| 487 | + /// <returns>图片上传结果,包含文件名和访问路径</returns> | |
| 488 | + /// <response code="200">图片上传成功</response> | |
| 489 | + /// <response code="400">请求参数错误或图片格式不支持</response> | |
| 490 | + /// <response code="500">服务器内部错误</response> | |
| 491 | + [HttpPost("UploadBase64Image")] | |
| 492 | + [AllowAnonymous] | |
| 493 | + public async Task<dynamic> UploadBase64Image([FromBody] Base64ImageUploadInput input) | |
| 494 | + { | |
| 495 | + try | |
| 496 | + { | |
| 497 | + // 验证输入参数 | |
| 498 | + if (string.IsNullOrEmpty(input.Base64Data)) | |
| 499 | + { | |
| 500 | + throw NCCException.Oh("Base64数据不能为空"); | |
| 501 | + } | |
| 502 | + | |
| 503 | + // 解析Base64数据 | |
| 504 | + var imageData = ParseBase64Data(input.Base64Data, out string imageFormat); | |
| 505 | + | |
| 506 | + // 验证图片格式 | |
| 507 | + if (!IsValidImageFormat(imageFormat)) | |
| 508 | + { | |
| 509 | + throw NCCException.Oh($"不支持的图片格式: {imageFormat}"); | |
| 510 | + } | |
| 511 | + | |
| 512 | + // 生成文件名 | |
| 513 | + var fileName = GenerateImageFileName(input.FileName, imageFormat); | |
| 514 | + | |
| 515 | + // 获取存储路径 | |
| 516 | + var imageType = string.IsNullOrEmpty(input.ImageType) ? "temporary" : input.ImageType; | |
| 517 | + var filePath = GetPathByType(imageType); | |
| 518 | + | |
| 519 | + // 确保目录存在 | |
| 520 | + if (!Directory.Exists(filePath)) | |
| 521 | + { | |
| 522 | + Directory.CreateDirectory(filePath); | |
| 523 | + } | |
| 524 | + | |
| 525 | + // 保存图片文件 | |
| 526 | + var fullPath = Path.Combine(filePath, fileName); | |
| 527 | + await File.WriteAllBytesAsync(fullPath, imageData); | |
| 528 | + | |
| 529 | + // 生成访问URL | |
| 530 | + var accessUrl = $"/api/File/Image/{imageType}/{fileName}"; | |
| 531 | + | |
| 532 | + return new | |
| 533 | + { | |
| 534 | + name = fileName, | |
| 535 | + url = accessUrl, | |
| 536 | + fileSize = imageData.Length, | |
| 537 | + imageFormat = imageFormat.ToUpper(), | |
| 538 | + imageType = imageType, | |
| 539 | + }; | |
| 540 | + } | |
| 541 | + catch (Exception ex) | |
| 542 | + { | |
| 543 | + throw NCCException.Oh($"Base64图片上传失败: {ex.Message}", ex); | |
| 544 | + } | |
| 545 | + } | |
| 546 | + | |
| 547 | + /// <summary> | |
| 548 | + /// 解析Base64数据并提取图片格式 | |
| 549 | + /// </summary> | |
| 550 | + /// <param name="base64Data">Base64数据</param> | |
| 551 | + /// <param name="imageFormat">输出图片格式</param> | |
| 552 | + /// <returns>解码后的图片字节数组</returns> | |
| 553 | + [NonAction] | |
| 554 | + private byte[] ParseBase64Data(string base64Data, out string imageFormat) | |
| 555 | + { | |
| 556 | + try | |
| 557 | + { | |
| 558 | + // 处理data:image/xxx;base64,格式 | |
| 559 | + if (base64Data.StartsWith("data:image/")) | |
| 560 | + { | |
| 561 | + var parts = base64Data.Split(','); | |
| 562 | + if (parts.Length != 2) | |
| 563 | + { | |
| 564 | + throw new ArgumentException("Base64数据格式不正确"); | |
| 565 | + } | |
| 566 | + | |
| 567 | + // 提取图片格式 | |
| 568 | + var header = parts[0]; | |
| 569 | + var formatMatch = Regex.Match(header, @"data:image/([^;]+)"); | |
| 570 | + if (formatMatch.Success) | |
| 571 | + { | |
| 572 | + imageFormat = formatMatch.Groups[1].Value.ToLower(); | |
| 573 | + } | |
| 574 | + else | |
| 575 | + { | |
| 576 | + imageFormat = "jpeg"; // 默认格式 | |
| 577 | + } | |
| 578 | + | |
| 579 | + // 解码Base64数据 | |
| 580 | + return Convert.FromBase64String(parts[1]); | |
| 581 | + } | |
| 582 | + else | |
| 583 | + { | |
| 584 | + // 纯Base64数据,默认为JPEG格式 | |
| 585 | + imageFormat = "jpeg"; | |
| 586 | + return Convert.FromBase64String(base64Data); | |
| 587 | + } | |
| 588 | + } | |
| 589 | + catch (Exception ex) | |
| 590 | + { | |
| 591 | + throw new ArgumentException($"Base64数据解析失败: {ex.Message}", ex); | |
| 592 | + } | |
| 593 | + } | |
| 594 | + | |
| 595 | + /// <summary> | |
| 596 | + /// 验证图片格式是否支持 | |
| 597 | + /// </summary> | |
| 598 | + /// <param name="imageFormat">图片格式</param> | |
| 599 | + /// <returns>是否支持</returns> | |
| 600 | + [NonAction] | |
| 601 | + private bool IsValidImageFormat(string imageFormat) | |
| 602 | + { | |
| 603 | + var allowedFormats = new[] { "jpg", "jpeg", "png", "gif", "bmp", "tiff", "webp" }; | |
| 604 | + return allowedFormats.Contains(imageFormat.ToLower()); | |
| 605 | + } | |
| 606 | + | |
| 607 | + /// <summary> | |
| 608 | + /// 生成图片文件名 | |
| 609 | + /// </summary> | |
| 610 | + /// <param name="inputFileName">输入的文件名</param> | |
| 611 | + /// <param name="imageFormat">图片格式</param> | |
| 612 | + /// <returns>生成的文件名</returns> | |
| 613 | + [NonAction] | |
| 614 | + private string GenerateImageFileName(string inputFileName, string imageFormat) | |
| 615 | + { | |
| 616 | + var timestamp = DateTime.Now.ToString("yyyyMMddHHmmss"); | |
| 617 | + var id = YitIdHelper.NextId().ToString(); | |
| 618 | + | |
| 619 | + if (!string.IsNullOrEmpty(inputFileName)) | |
| 620 | + { | |
| 621 | + // 移除可能存在的扩展名 | |
| 622 | + var nameWithoutExt = Path.GetFileNameWithoutExtension(inputFileName); | |
| 623 | + return $"{nameWithoutExt}_{timestamp}_{id}.{imageFormat}"; | |
| 624 | + } | |
| 625 | + else | |
| 626 | + { | |
| 627 | + return $"image_{timestamp}_{id}.{imageFormat}"; | |
| 628 | + } | |
| 629 | + } | |
| 630 | + | |
| 631 | + #endregion | |
| 449 | 632 | } |
| 450 | 633 | } | ... | ... |