from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
from docx import Document
from docx.shared import Inches
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_BREAK

ROOT=Path('/Users/lqt/work/项目/张家港')
OUT=ROOT/'张家港智慧餐厅系统部署与对接方案（重新梳理版）.docx'
ASSET=ROOT/'方案图（重新梳理版）'
ASSET.mkdir(parents=True,exist_ok=True)
W,H,M=1547,2002,135
BLUE,NAVY,TEXT,MUTED,LINE,PALE,GREEN='#2877A8','#183B56','#333333','#6C737A','#D9E0E5','#F5F8FA','#EDF6F0'
paths=['/System/Library/Fonts/PingFang.ttc','/System/Library/Fonts/STHeiti Light.ttc','/System/Library/Fonts/Hiragino Sans GB.ttc']
FP=next(p for p in paths if Path(p).exists())
def ft(n,b=False):
    try:return ImageFont.truetype(FP,n,index=2 if b and FP.endswith('.ttc') else 0)
    except:return ImageFont.truetype(FP,n)
F={'tiny':ft(19),'small':ft(22),'body':ft(27),'body_b':ft(27,1),'sub':ft(31,1),'h2':ft(38,1),'title':ft(58,1)}
def page(n):
    im=Image.new('RGB',(W,H),'white');d=ImageDraw.Draw(im)
    d.text((M,65),'张家港智慧餐厅项目  部署与对接方案',font=F['small'],fill=MUTED)
    d.line((M,103,W-M,103),fill=LINE,width=2);d.text((W-M-10,H-72),str(n),font=F['small'],fill=MUTED,anchor='ra')
    return im,d
def wrap(d,s,f,w):
    out=[]
    for raw in s.split('\n'):
        cur=''
        for c in raw:
            if d.textlength(cur+c,font=f)<=w or not cur:cur+=c
            else:out.append(cur);cur=c
        if cur:out.append(cur)
    return out
def para(d,x,y,s,w=None,f=None,fill=TEXT,gap=12,lead=1.55):
    w=w or W-2*M;f=f or F['body'];lh=int(f.size*lead)
    for raw in s.split('\n'):
        for line in wrap(d,raw,f,w):d.text((x,y),line,font=f,fill=fill);y+=lh
        y+=gap
    return y
def head(d,y,s):d.text((M,y),s,font=F['h2'],fill=BLUE);return y+66
def box(d,xy,s,fill='white',outline='#9BAEBB',f=None):
    f=f or F['body_b'];d.rounded_rectangle(xy,radius=12,fill=fill,outline=outline,width=2)
    x1,y1,x2,y2=xy;ls=wrap(d,s,f,x2-x1-28);yy=(y1+y2-len(ls)*int(f.size*1.25))/2
    for line in ls:d.text(((x1+x2)/2,yy),line,font=f,fill=TEXT,anchor='ma');yy+=int(f.size*1.25)
def arrow(d,a,b,label=None):
    import math
    d.line((*a,*b),fill=NAVY,width=4);ang=math.atan2(b[1]-a[1],b[0]-a[0])
    for da in (2.55,-2.55):
        p=(b[0]+18*math.cos(ang+da),b[1]+18*math.sin(ang+da));d.line((*b,*p),fill=NAVY,width=4)
    if label:d.text(((a[0]+b[0])/2,(a[1]+b[1])/2-24),label,font=F['tiny'],fill=MUTED,anchor='mm')
def bullet(d,y,t,b):
    d.ellipse((M+4,y+12,M+15,y+23),fill=BLUE);d.text((M+30,y),t,font=F['body_b'],fill=TEXT)
    return para(d,M+30,y+43,b,W-2*M-30,F['body'])+8
pages=[]

im,d=page(1)
d.text((M,155),'张家港智慧餐厅系统',font=F['sub'],fill=MUTED)
d.text((M,218),'部署与对接方案',font=F['title'],fill=NAVY)
d.text((M,305),'本地部署  两台AI识别结算台  卡码脸同步结算',font=F['body'],fill=MUTED)
y=head(d,385,'一 方案说明')
y=para(d,M,y,'智慧餐厅服务器部署在餐厅现场，与两台AI识别结算台通过餐厅局域网通信。服务器负责菜品数据、人员及人脸数据、订单和接口日志管理，并同步调用客户结算系统完成扣款。')
y=para(d,M,y,'人脸消费采用照片同步方案：客户提供人员及人脸照片，智慧餐厅服务器通过百度麒麟X86离线人脸SDK在本地完成人脸学习，再将特征值下发至两台结算台。人脸学习过程不需要服务器连接外网。')
d.text((W/2,720),'部署拓扑',font=F['sub'],fill=NAVY,anchor='mm')
box(d,(165,800,480,900),'客户人员与\n人脸照片数据',PALE);box(d,(1065,800,1375,900),'客户结算系统',PALE)
box(d,(605,1030,945,1140),'智慧餐厅\n本地服务器','#EAF2F7')
box(d,(165,1275,475,1375),'AI识别结算台 1',GREEN);box(d,(165,1490,475,1590),'AI识别结算台 2',GREEN)
box(d,(1070,1275,1375,1375),'本地数据库',PALE);box(d,(1070,1490,1375,1590),'接口及运行日志',PALE)
arrow(d,(480,850),(650,1030),'全量/增量照片');arrow(d,(1065,825),(920,1030),'结算结果');arrow(d,(945,1070),(1150,900),'同步结算')
arrow(d,(605,1080),(475,1325));arrow(d,(475,1360),(610,1110))
arrow(d,(605,1120),(475,1540));arrow(d,(475,1575),(635,1135))
d.text((430,1190),'下发菜品和人脸数据  返回订单和结算结果',font=F['tiny'],fill=MUTED)
arrow(d,(945,1090),(1070,1325));arrow(d,(945,1125),(1070,1540))
d.text((W/2,1745),'图1  部署拓扑图',font=F['small'],fill=MUTED,anchor='mm');pages.append(im)

im,d=page(2);y=head(d,155,'二 前置条件')
y=bullet(d,y,'菜品学习','在AI识别结算台进行菜品学习。学习完成后，菜品图片、菜品信息及价格等数据推送到智慧餐厅本地服务器。')
y=bullet(d,y,'人员及人脸照片同步','客户结算系统提供人员及人脸照片的首次全量和后续增量获取接口。新增人员、照片更新或人员状态变化时，通过增量接口同步。')
y=bullet(d,y,'本地离线人脸学习','智慧餐厅本地服务器部署百度麒麟X86离线人脸SDK，使用客户提供的人脸照片提取特征值，并将人员信息及特征值下发至两台AI识别结算台。')
d.text((W/2,680),'前置条件流程',font=F['sub'],fill=NAVY,anchor='mm')
box(d,(155,770,450,885),'AI结算台\n菜品学习',GREEN);box(d,(625,770,920,885),'菜品图片\n信息及价格',PALE);box(d,(1095,770,1390,885),'智慧餐厅\n本地服务器','#EAF2F7')
arrow(d,(450,827),(625,827),'推送');arrow(d,(920,827),(1095,827))
box(d,(155,1060,450,1175),'客户结算系统',PALE);box(d,(565,1060,910,1175),'人员及人脸照片\n全量/增量获取',PALE);box(d,(1040,1060,1390,1175),'本地离线\n人脸学习','#EAF2F7')
arrow(d,(450,1117),(565,1117));arrow(d,(910,1117),(1040,1117))
box(d,(510,1370,880,1485),'智慧餐厅本地服务器','#EAF2F7');box(d,(1035,1300,1390,1405),'AI结算台 1',GREEN);box(d,(1035,1470,1390,1575),'AI结算台 2',GREEN)
arrow(d,(1215,1175),(760,1370),'生成并保存特征值');arrow(d,(880,1420),(1035,1352),'统一下发');arrow(d,(880,1445),(1035,1522))
d.text((W/2,1745),'图2  前置条件流程图',font=F['small'],fill=MUTED,anchor='mm');pages.append(im)

im,d=page(3);y=head(d,155,'三 使用流程')
steps=['用户取餐后，将菜品放到AI识别结算台。','结算台识别菜品，展示菜品明细和结算金额。','用户选择刷卡、扫码或刷脸进行身份确认。','结算台生成订单，将身份标识、结算金额和订单信息推送到智慧餐厅本地服务器。','本地服务器通过SM4加密后，同步调用客户结算系统核销接口。','客户结算系统返回成功或失败结果，本地服务器将结果返回结算台展示。']
for i,s in enumerate(steps,1):
    d.text((M,y),f'{i}.',font=F['body_b'],fill=BLUE);y=para(d,M+42,y,s,W-2*M-42,F['body'],gap=6,lead=1.45)+4
d.text((W/2,690),'使用流程',font=F['sub'],fill=NAVY,anchor='mm')
nodes=[('用户取餐',150,780),('AI识别菜品',445,780),('获取结算价格',740,780),('刷卡/码/脸',1035,780)]
for t,x,yy in nodes:box(d,(x,yy,x+245,yy+100),t)
for i in range(3):arrow(d,(nodes[i][1]+245,830),(nodes[i+1][1],830))
box(d,(1020,1040,1390,1150),'AI结算台生成订单',GREEN);arrow(d,(1157,880),(1205,1040))
box(d,(610,1280,955,1390),'智慧餐厅本地服务器','#EAF2F7');arrow(d,(1080,1150),(880,1280),'推送订单')
box(d,(150,1280,480,1390),'客户结算系统',PALE);arrow(d,(610,1335),(480,1335),'SM4同步核销');arrow(d,(480,1380),(610,1380),'返回结果')
box(d,(150,1570,480,1680),'结算台展示结果',GREEN);arrow(d,(610,1390),(400,1570),'返回结算台')
d.text((W/2,1810),'图3  使用流程图',font=F['small'],fill=MUTED,anchor='mm');pages.append(im)

im,d=page(4);y=head(d,155,'四 双方接口')
y=para(d,M,y,'本项目需要三类接口。目前客户已提供核销接口文档；人员及人脸照片的全量、增量接口仍需补充。')
cols=[M,210,720,1120,W-M];top=355;rh=94
for i,h in enumerate(['序号','接口','用途','当前状态']):
    d.rectangle((cols[i],top,cols[i+1],top+rh),fill=BLUE,outline=LINE,width=2);d.text(((cols[i]+cols[i+1])/2,top+rh/2),h,font=F['body_b'],fill='white',anchor='mm')
rows=[('1','人员及人脸照片全量接口','首次获取全部人员与照片','待客户提供'),('2','人员及人脸照片增量接口','获取新增、更新及状态变化','待客户提供'),('3','卡码脸同步结算接口','按身份标识和金额同步扣款','已提供初稿')]
for r,row in enumerate(rows):
    yy=top+rh*(r+1);fill='white' if r%2==0 else PALE
    for i,val in enumerate(row):
        d.rectangle((cols[i],yy,cols[i+1],yy+rh),fill=fill,outline=LINE,width=2);ls=wrap(d,val,F['small'],cols[i+1]-cols[i]-24);sy=yy+rh/2-(len(ls)-1)*15
        for line in ls:d.text(((cols[i]+cols[i+1])/2,sy),line,font=F['small'],fill=TEXT,anchor='mm');sy+=30
y=head(d,top+rh*4+70,'五 已提供核销接口说明')
y=bullet(d,y,'接口地址','IP:端口/mngcanteen/mngbase/system/qrcode/moneynew/cantnEncryptHX')
y=bullet(d,y,'请求内容','外层参数为securityParam；加密前明文为 {"code":"xxxx","amt":"10"}。code可表示二维码code、部门卡ID或员工工号，amt表示消费金额。')
y=bullet(d,y,'加密方式','SM4/ECB/PKCS5Padding，密文使用十六进制字符串传输。')
y=bullet(d,y,'返回结果','code为200表示成功，其余表示失败；msg返回错误信息。')
y=head(d,y,'六 核销接口需确认项')
para(d,M,y,'请求方法及Content-Type、卡码脸对应的code取值、金额单位、订单号及防重复扣款规则、业务错误码、超时时间、测试与正式地址、SM4密钥交付方式及完整请求响应示例。')
pages.append(im)

im,d=page(5);y=head(d,155,'七 服务器及网络要求')
y=para(d,M,y,'按约200人用餐、两台AI识别结算台的基本业务量，服务器可采用以下配置，后续根据人脸数量、照片容量和实际运行负载进行扩容。')
specs=[('CPU架构','x86_64'),('CPU','4核'),('内存','16GB'),('硬盘','200GB SSD'),('网卡','单千兆网口'),('操作系统','麒麟Linux服务器版')]
top=400;c1=M;c2=510;c3=W-M;rh=82
for r,(a,b) in enumerate(specs):
    yy=top+r*rh;fill='white' if r%2==0 else PALE
    d.rectangle((c1,yy,c2,yy+rh),fill=fill,outline=LINE,width=2);d.rectangle((c2,yy,c3,yy+rh),fill=fill,outline=LINE,width=2)
    d.text(((c1+c2)/2,yy+rh/2),a,font=F['body_b'],fill=TEXT,anchor='mm');d.text((c2+35,yy+rh/2),b,font=F['body'],fill=TEXT,anchor='lm')
y=top+len(specs)*rh+65
y=para(d,M,y,'服务器需兼容百度麒麟X86离线人脸SDK。单千兆网口可满足当前两台结算台和基本接口通信需求；如后续需要设备网与业务网隔离或链路冗余，可增配第二块网卡。')
y=head(d,y,'八 网络边界')
y=bullet(d,y,'本地人脸处理','人员照片、人脸学习和特征值下发均可在餐厅局域网内完成，不需要服务器连接互联网。')
y=bullet(d,y,'结算接口调用','如果客户结算接口部署在本地内网，双方只需保证内网互通；如果接口部署在客户云端，则需要为智慧餐厅服务器开通受控访问通道，可采用专线、VPN或指定地址及端口白名单。')
y=head(d,y,'九 双方工作分工')
for t,b in [('智慧餐厅系统方','部署本地服务器、离线人脸服务和两台结算台；完成菜品学习、人脸照片学习、特征值下发、订单推送及核销接口调用。'),('客户结算系统方','提供人员及人脸照片全量与增量接口，完善同步核销接口及测试环境，并明确身份标识、金额、密钥和返回码规则。'),('双方共同','完成网络开通、接口联调，并验证菜品识别、刷卡、扫码、刷脸和同步扣款结果。')]:y=bullet(d,y,t,b)
pages.append(im)

imgs=[]
for i,im in enumerate(pages,1):
    p=ASSET/f'page-{i}.png';im.save(p,quality=95);imgs.append(p)
doc=Document();sec=doc.sections[0]
sec.page_width=Inches(8.5);sec.page_height=Inches(11);sec.top_margin=sec.bottom_margin=sec.left_margin=sec.right_margin=Inches(0)
for i,p in enumerate(imgs):
    q=doc.add_paragraph();q.paragraph_format.space_before=q.paragraph_format.space_after=0;q.alignment=WD_ALIGN_PARAGRAPH.CENTER;q.add_run().add_picture(str(p),width=Inches(8.3),height=Inches(10.74))
    if i<len(imgs)-1:q.add_run().add_break(WD_BREAK.PAGE)
body=doc._element.body;sp=body.sectPr;body.remove(sp);body.append(sp)
doc.save(OUT);print(OUT)
